#!/usr/bin/env python3
"""
Script to generate Mintlify-compatible MDX API documentation files and update docs.json.
Organizes files by path structure and excludes __init__.py files.
"""

import ast
import inspect
import json
import os
import sys
from pathlib import Path
from typing import Any, Union


def detect_deprecated_items(file_path: str) -> list[str]:
    """Detect deprecated classes and functions in a Python file.

    Args:
        file_path: Path to the Python file to analyze

    Returns:
        List of deprecated class/function names
    """
    deprecated_items = []

    try:
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()

        tree = ast.parse(content)

        # Check for @deprecated decorators
        for node in ast.walk(tree):
            if isinstance(node, ast.ClassDef):
                for decorator in node.decorator_list:
                    if isinstance(decorator, ast.Name) and decorator.id == "deprecated":
                        deprecated_items.append(node.name)
                    elif (
                        isinstance(decorator, ast.Call)
                        and isinstance(decorator.func, ast.Name)
                        and decorator.func.id == "deprecated"
                    ):
                        deprecated_items.append(node.name)
            elif isinstance(node, ast.FunctionDef):
                for decorator in node.decorator_list:
                    if isinstance(decorator, ast.Name) and decorator.id == "deprecated":
                        deprecated_items.append(node.name)
                    elif (
                        isinstance(decorator, ast.Call)
                        and isinstance(decorator.func, ast.Name)
                        and decorator.func.id == "deprecated"
                    ):
                        deprecated_items.append(node.name)

    except (FileNotFoundError, SyntaxError, UnicodeDecodeError) as e:
        print(f"Warning: Could not analyze {file_path}: {e}")

    return deprecated_items


def detect_deprecated_imports(file_path: str) -> list[dict[str, str]]:
    """Detect deprecated import paths in a Python file by analyzing @deprecated decorators.

    Args:
        file_path: Path to the Python file to analyze

    Returns:
        List of dictionaries containing deprecated import information
    """
    deprecated_imports = []

    try:
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()

        tree = ast.parse(content)

        # Extract deprecated patterns from @deprecated decorators
        deprecated_patterns = {}

        for node in ast.walk(tree):
            if isinstance(node, (ast.ClassDef, ast.FunctionDef)):
                for decorator in node.decorator_list:
                    if (
                        isinstance(decorator, ast.Call)
                        and isinstance(decorator.func, ast.Name)
                        and decorator.func.id == "deprecated"
                    ):
                        # Extract the deprecation message
                        if decorator.args and isinstance(
                            decorator.args[0], ast.Constant
                        ):
                            deprecation_msg = decorator.args[0].value
                            if isinstance(
                                deprecation_msg, str
                            ) and deprecation_msg.startswith("Use "):
                                # Extract the new path from "Use mcp_use.new.path"
                                new_path = deprecation_msg[4:]  # Remove "Use "

                                # Determine the old path based on current file location
                                old_path = _get_current_module_path(file_path)

                                if old_path and new_path != old_path:
                                    deprecated_patterns[old_path] = new_path

        # Now check for imports that match these patterns
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    if alias.name in deprecated_patterns:
                        deprecated_imports.append(
                            {
                                "type": "import",
                                "line": node.lineno,
                                "old_path": alias.name,
                                "new_path": deprecated_patterns[alias.name],
                                "alias": alias.asname,
                            }
                        )
            elif isinstance(node, ast.ImportFrom):
                if node.module and node.module in deprecated_patterns:
                    deprecated_imports.append(
                        {
                            "type": "from_import",
                            "line": node.lineno,
                            "old_path": node.module,
                            "new_path": deprecated_patterns[node.module],
                            "names": [alias.name for alias in node.names],
                        }
                    )

    except (FileNotFoundError, SyntaxError, UnicodeDecodeError) as e:
        print(f"Warning: Could not analyze {file_path}: {e}")

    return deprecated_imports


def _get_current_module_path(file_path: str) -> str:
    """Extract the module path from a file path."""
    # Convert file path to module path
    # e.g., "../libraries/python/mcp_use/session.py" -> "mcp_use.session"
    path_parts = file_path.replace("\\", "/").split("/")

    # Find the mcp_use directory
    try:
        mcp_use_index = path_parts.index("mcp_use")
        module_parts = path_parts[mcp_use_index:]

        # Remove .py extension and join with dots
        if module_parts[-1].endswith(".py"):
            module_parts[-1] = module_parts[-1][:-3]

        return ".".join(module_parts)
    except ValueError:
        return None


def generate_deprecation_warning(
    deprecated_imports: list[dict[str, str]], file_path: str
) -> str:
    """Generate a deprecation warning callout for deprecated imports.

    Args:
        deprecated_imports: List of deprecated import information
        file_path: Path to the file being analyzed

    Returns:
        MDX formatted deprecation warning
    """
    if not deprecated_imports:
        return ""

    warning_lines = [
        "<Warning>",
        f"**File:** `{file_path}`",
        "",
        "The following import paths are deprecated and should be updated:",
        "",
    ]

    for dep_import in deprecated_imports:
        if dep_import["type"] == "import":
            old_line = f"import {dep_import['old_path']}"
            if dep_import["alias"]:
                old_line += f" as {dep_import['alias']}"
            new_line = f"import {dep_import['new_path']}"
            if dep_import["alias"]:
                new_line += f" as {dep_import['alias']}"

            warning_lines.extend(
                [
                    f"**Line {dep_import['line']}:**",
                    "```python",
                    "# Deprecated:",
                    f"{old_line}",
                    "# Use instead:",
                    f"{new_line}",
                    "```",
                    "",
                ]
            )
        elif dep_import["type"] == "from_import":
            names_str = ", ".join(dep_import["names"])
            old_line = f"from {dep_import['old_path']} import {names_str}"
            new_line = f"from {dep_import['new_path']} import {names_str}"

            warning_lines.extend(
                [
                    f"**Line {dep_import['line']}:**",
                    "```python",
                    "# Deprecated:",
                    f"{old_line}",
                    "# Use instead:",
                    f"{new_line}",
                    "```",
                    "",
                ]
            )

    warning_lines.append("</Warning>")
    warning_lines.append("")

    return "\n".join(warning_lines)


def get_docstring(obj: Any) -> str:
    """Extract docstring from an object."""
    if hasattr(obj, "__doc__") and obj.__doc__:
        return obj.__doc__.strip()
    return ""


def format_type_annotation(annotation: Any) -> str:
    """Format type annotation for display."""
    if annotation is None or annotation is type(None):
        return "None"

    # Handle GenericAlias (Python 3.9+)
    if hasattr(annotation, "__origin__") and hasattr(annotation, "__args__"):
        origin = annotation.__origin__
        args = annotation.__args__

        if origin is Union:
            if args and args[-1] is type(None):
                # Optional type
                non_none_args = [arg for arg in args if arg is not type(None)]
                if len(non_none_args) == 1:
                    return f"{format_type_annotation(non_none_args[0])} | None"
                else:
                    return (
                        " | ".join(format_type_annotation(arg) for arg in non_none_args)
                        + " | None"
                    )
            else:
                return " | ".join(format_type_annotation(arg) for arg in args)
        elif origin is list:
            if args:
                return f"list[{format_type_annotation(args[0])}]"
            return "list"
        elif origin is dict:
            if len(args) >= 2:
                return f"dict[{format_type_annotation(args[0])}, {format_type_annotation(args[1])}]"
            return "dict"
        elif origin is tuple:
            if args:
                return (
                    f"tuple[{', '.join(format_type_annotation(arg) for arg in args)}]"
                )
            return "tuple"
        elif origin is set:
            if args:
                return f"set[{format_type_annotation(args[0])}]"
            return "set"

    # Handle regular types - show full import path
    if hasattr(annotation, "__module__") and hasattr(annotation, "__name__"):
        module = annotation.__module__
        name = annotation.__name__
        # Skip built-in types and common stdlib types
        if module in ("builtins", "typing", "collections.abc") or module.startswith(
            "typing"
        ):
            return name
        # For external modules, show the full path
        return f"{module}.{name}"
    elif hasattr(annotation, "__name__"):
        return annotation.__name__
    elif isinstance(annotation, str):
        return annotation

    return str(annotation)


def process_docstring(docstring: str) -> str:
    """Process docstring to convert code blocks, escape problematic characters, and remove Args section."""
    if not docstring:
        return ""

    lines = docstring.split("\n")
    processed_lines = []
    in_code_block = False
    in_args_section = False
    skip_line = False

    for _, line in enumerate(lines):
        stripped = line.strip()

        # Skip if we're in a line that should be skipped
        if skip_line:
            skip_line = False
            continue

        # Handle Sphinx-style code blocks
        if stripped.startswith(".. code-block::"):
            in_code_block = True
            processed_lines.append("```python wrap")
            continue
        elif (
            in_code_block
            and stripped
            and not line.startswith(" ")
            and not line.startswith("\t")
        ):
            # End of code block
            in_code_block = False
            processed_lines.append("```")
            processed_lines.append("")
            processed_lines.append(line)
            continue
        elif in_code_block:
            # Inside code block - escape curly braces in comments
            if "#" in line:
                line = line.replace("{", "\\{").replace("}", "\\}")
            processed_lines.append(line)
        else:
            # Check for Args:, Parameters:, Returns:, Warns:, Raises: sections
            if stripped.lower().startswith(
                (
                    "args:",
                    "parameters:",
                    "returns:",
                    "return:",
                    "warns:",
                    "warn:",
                    "raises:",
                    "raise:",
                )
            ):
                in_args_section = True
                skip_line = True  # Skip the section header line itself
                continue
            # Check for end of section
            elif in_args_section and stripped.lower().startswith(
                ("yields:", "note:", "example:", "usage:")
            ):
                in_args_section = False
                processed_lines.append(line)
            # Skip lines in args section
            elif in_args_section:
                # Check if this line starts a new section (not indented)
                if (
                    stripped
                    and not line.startswith(" ")
                    and not line.startswith("\t")
                    and ":" in stripped
                ):
                    in_args_section = False
                    processed_lines.append(line)
                # Otherwise skip this line (it's part of args section)
                continue
            else:
                # Outside code block and args section - escape curly braces
                line = line.replace("{", "\\{").replace("}", "\\}")
                processed_lines.append(line)

    # Close any open code block
    if in_code_block:
        processed_lines.append("```")

    return "\n".join(processed_lines)


def format_signature(func: Any) -> str:
    """Format function signature for display."""
    try:
        sig = inspect.signature(func)
        params = []
        for name, param in sig.parameters.items():
            if name == "self":
                continue
            param_str = name
            if param.annotation != inspect.Signature.empty:
                param_str += f": {format_type_annotation(param.annotation)}"
            if param.default != inspect.Signature.empty:
                if isinstance(param.default, str):
                    param_str += f' = "{param.default}"'
                else:
                    param_str += f" = {param.default}"
            params.append(param_str)
        return f"({', '.join(params)})"
    except Exception:
        return "()"


def extract_param_docs(docstring: str) -> dict[str, str]:
    """Extract parameter descriptions from docstring."""
    param_docs = {}
    if not docstring:
        return param_docs

    lines = docstring.split("\n")
    current_param = None
    current_desc = []
    in_args_section = False

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

        # Sphinx style: :param name: description
        if stripped.startswith(":param "):
            if current_param and current_desc:
                param_docs[current_param] = " ".join(current_desc).strip()
            parts = stripped[7:].split(":", 1)
            if len(parts) == 2:
                current_param = parts[0].strip()
                current_desc = [parts[1].strip()] if parts[1].strip() else []
            else:
                current_param = parts[0].strip()
                current_desc = []
        # Google style: Args: or Parameters:
        elif stripped.lower().startswith(("args:", "parameters:")):
            in_args_section = True
            current_param = None
            current_desc = []
        # Google style: name (type): description
        elif in_args_section and ":" in stripped and not stripped.startswith(" "):
            if current_param and current_desc:
                param_docs[current_param] = " ".join(current_desc).strip()
            parts = stripped.split(":", 1)
            if len(parts) == 2:
                param_part = parts[0].strip()
                if "(" in param_part and ")" in param_part:
                    param_name = param_part.split("(")[0].strip()
                    current_param = param_name
                    current_desc = [parts[1].strip()] if parts[1].strip() else []
                else:
                    # Simple parameter name without type
                    current_param = param_part
                    current_desc = [parts[1].strip()] if parts[1].strip() else []
        # Continuation of current parameter description
        elif current_param and (
            stripped.startswith(" ") or stripped == "" or stripped.startswith("-")
        ):
            if stripped:
                current_desc.append(stripped)
        # End of args section
        elif in_args_section and stripped.lower().startswith(
            ("returns:", "raises:", "yields:", "note:", "example:")
        ):
            if current_param and current_desc:
                param_docs[current_param] = " ".join(current_desc).strip()
            in_args_section = False
            current_param = None
            current_desc = []
        else:
            if current_param and current_desc:
                param_docs[current_param] = " ".join(current_desc).strip()
            current_param = None
            current_desc = []

    # Add the last parameter
    if current_param and current_desc:
        param_docs[current_param] = " ".join(current_desc).strip()

    return param_docs


def generate_param_field(name: str, param: inspect.Parameter, description: str) -> str:
    """Generate ParamField component for a parameter."""
    param_type = ""
    default_value = ""

    if param.annotation != inspect.Signature.empty:
        param_type = format_type_annotation(param.annotation)

    if param.default != inspect.Signature.empty:
        if isinstance(param.default, str):
            # Use single quotes for string defaults to avoid JSX parsing issues
            default_value = f"'{param.default}'"
        else:
            # Always quote non-string defaults to avoid JSX parsing issues
            default_value = f'"{str(param.default)}"'

    field_parts = [f'<ParamField body="{name}"']

    if param_type:
        field_parts.append(f'type="{param_type}"')

    if default_value:
        field_parts.append(f"default={default_value}")
    else:
        # Add required="True" for parameters without default values
        field_parts.append('required="True"')

    field_parts.append(">")

    if description and description.strip():
        field_parts.append(f"  {description.strip()}")
    else:
        # Provide more helpful default descriptions based on parameter name
        default_desc = get_default_param_description(name, param_type)
        field_parts.append(f"  {default_desc}")

    field_parts.append("</ParamField>")

    return " ".join(field_parts)


def get_default_param_description(name: str, param_type: str) -> str:
    """Generate a helpful default description for a parameter."""
    name_lower = name.lower()

    # Common parameter patterns
    if "config" in name_lower:
        return "Configuration object or file path"
    elif "session" in name_lower:
        return "MCP session instance"
    elif "client" in name_lower:
        return "MCP client instance"
    elif "server" in name_lower:
        return "Server name or configuration"
    elif "query" in name_lower:
        return "Query string or input"
    elif "callback" in name_lower:
        return "Callback function"
    elif "middleware" in name_lower:
        return "Middleware instance"
    elif "connector" in name_lower:
        return "Connector instance"
    elif "tools" in name_lower:
        return "List of tools"
    elif "name" in name_lower:
        return "Name identifier"
    elif "path" in name_lower or "filepath" in name_lower:
        return "File path"
    elif "url" in name_lower:
        return "URL string"
    elif "timeout" in name_lower:
        return "Timeout duration"
    elif "retries" in name_lower:
        return "Number of retry attempts"
    elif "debug" in name_lower or "verbose" in name_lower:
        return "Enable debug/verbose mode"
    elif "async" in name_lower:
        return "Enable asynchronous mode"
    elif "auto" in name_lower:
        return "Enable automatic behavior"
    elif param_type and "bool" in param_type.lower():
        return "Boolean flag"
    elif param_type and "list" in param_type.lower():
        return "List of items"
    elif param_type and "dict" in param_type.lower():
        return "Dictionary of key-value pairs"
    elif param_type and "str" in param_type.lower():
        return "String value"
    elif param_type and "int" in param_type.lower():
        return "Integer value"
    else:
        return "Parameter value"


def extract_return_docs(docstring: str) -> str:
    """Extract return value description from docstring."""
    if not docstring:
        return ""

    lines = docstring.split("\n")
    in_returns_section = False
    return_desc = []

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

        # Sphinx style: :returns: or :return:
        if stripped.startswith((":returns:", ":return:")):
            return_desc.append(stripped.split(":", 1)[1].strip())
            in_returns_section = True
        # Google style: Returns: or Return: (handle indented)
        elif stripped.lower().endswith(("returns:", "return:")):
            # Extract the description part after the colon
            desc_part = stripped.split(":", 1)[1].strip()
            if desc_part:
                return_desc.append(desc_part)
            in_returns_section = True
        # Continuation of return description
        elif in_returns_section and (
            line.startswith(" ") or stripped == "" or stripped.startswith("-")
        ):
            # Check if this line starts a new section (like "Example:")
            if stripped and stripped.lower().endswith(":"):
                break
            if stripped:
                return_desc.append(stripped)
        # End of returns section
        elif in_returns_section and stripped.lower().startswith(
            (
                "raises:",
                "yields:",
                "note:",
                "example:",
                "args:",
                "parameters:",
                "warns:",
                "warn:",
            )
        ):
            break

    return " ".join(return_desc).strip()


def extract_warns_docs(docstring: str) -> list[str]:
    """Extract warns documentation from docstring."""
    if not docstring:
        return []

    lines = docstring.split("\n")
    in_warns_section = False
    warn_desc = []

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

        # Sphinx style: :warns: or :warn:
        if stripped.startswith((":warns:", ":warn:")):
            warn_desc.append(stripped.split(":", 1)[1].strip())
            in_warns_section = True
        # Google style: Warns: or Warn:
        elif stripped.lower().startswith(("warns:", "warn:")):
            warn_desc.append(stripped.split(":", 1)[1].strip())
            in_warns_section = True
        # Continuation of warn description
        elif in_warns_section and (
            stripped.startswith(" ") or stripped == "" or stripped.startswith("-")
        ):
            if stripped:
                warn_desc.append(stripped)
        # End of warns section
        elif in_warns_section and stripped.lower().startswith(
            (
                "raises:",
                "yields:",
                "note:",
                "example:",
                "args:",
                "parameters:",
                "returns:",
                "return:",
            )
        ):
            break

    return warn_desc


def extract_raises_docs(docstring: str) -> list[str]:
    """Extract raises documentation from docstring."""
    if not docstring:
        return []

    lines = docstring.split("\n")
    in_raises_section = False
    raise_desc = []

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

        # Sphinx style: :raises: or :raise:
        if stripped.startswith((":raises:", ":raise:")):
            raise_desc.append(stripped.split(":", 1)[1].strip())
            in_raises_section = True
        # Google style: Raises: or Raise:
        elif stripped.lower().startswith(("raises:", "raise:")):
            raise_desc.append(stripped.split(":", 1)[1].strip())
            in_raises_section = True
        # Continuation of raise description
        elif in_raises_section and (
            stripped.startswith(" ") or stripped == "" or stripped.startswith("-")
        ):
            if stripped:
                raise_desc.append(stripped)
        # End of raises section
        elif in_raises_section and stripped.lower().startswith(
            (
                "warns:",
                "warn:",
                "yields:",
                "note:",
                "example:",
                "args:",
                "parameters:",
                "returns:",
                "return:",
            )
        ):
            break

    return raise_desc


def generate_module_description(module_name: str, module_docstring: str) -> str:
    """Generate a better description for a module."""
    if module_docstring and module_docstring.strip():
        # Use the first sentence of the docstring
        first_sentence = module_docstring.strip().split(".")[0].strip()
        if first_sentence:
            return f"{first_sentence} API Documentation"

    # Fallback to generic description
    return f"{module_name.split('.')[-1].replace('_', ' ').title()} API Documentation"


def generate_response_field(name: str, field_type: str, description: str = "") -> str:
    """Generate ResponseField component with enhanced formatting."""
    field_parts = [f'<ResponseField name="{name}"']

    if field_type and field_type.strip():
        field_parts.append(f'type="{field_type}"')

    if description:
        # Escape quotes in description
        escaped_desc = description.replace('"', "&quot;")
        field_parts.append(f">{escaped_desc}</ResponseField>")
    else:
        field_parts.append("/>")

    return " ".join(field_parts)


def is_defined_in_module(obj: Any, module_name: str) -> bool:
    """Check if an object is defined in the specified module."""
    if hasattr(obj, "__module__"):
        return obj.__module__ == module_name
    return False


def generate_class_docs(cls: type, module_name: str) -> str:
    """Generate documentation for a class."""
    docs = []

    # Class header
    class_name = cls.__name__
    docstring = get_docstring(cls)

    # Class section header for sidebar
    docs.append(f"## {class_name}")
    docs.append("")

    # Class Card with gradient background
    docs.append("<div>")
    docs.append(
        '<RandomGradientBackground className="rounded-lg p-4 w-full h-full rounded-full">'
    )
    docs.append('<div className="text-black">')
    docs.append(
        f'<div className="text-black font-bold text-xl mb-2 mt-8">'
        f'<code className="!text-black">class</code> {class_name}</div>'
    )
    docs.append("")

    if docstring:
        docs.append(process_docstring(docstring))
        docs.append("")
    docs.append("</div>")
    docs.append("</RandomGradientBackground>")

    # Add import example below gradient
    docs.append("```python")
    docs.append(f"from {module_name} import {class_name}")
    docs.append("```")
    docs.append("")

    # Class attributes/fields - focus on type annotations for Pydantic models
    class_attributes = []

    # Check for annotations that are likely user-defined fields
    if hasattr(cls, "__annotations__"):
        for name, annotation in cls.__annotations__.items():
            # Skip private attributes and Pydantic internal fields
            if (
                not name.startswith("_")
                and not name.startswith("model_")
                and name
                not in ["computed_fields", "config", "extra", "fields", "fields_set"]
            ):
                class_attributes.append((name, annotation))

    if class_attributes:
        docs.append('<Card type="info">')
        docs.append("**Attributes**")
        docs.append(">")
        for name, annotation in class_attributes:
            # Create a mock parameter object for the annotation
            from inspect import Parameter

            param = Parameter(
                name, Parameter.POSITIONAL_OR_KEYWORD, annotation=annotation
            )
            param_field = generate_param_field(name, param, "")
            docs.append(param_field)
        docs.append("")
        docs.append("</Card>")
        docs.append("")

    # Constructor
    if hasattr(cls, "__init__"):
        init_method = cls.__init__
        if init_method != object.__init__:
            # Constructor Card
            docs.append('<Card type="info">')
            docs.append("### `method` __init__")
            docs.append("")

            try:
                init_docstring = get_docstring(init_method)
                sig = inspect.signature(init_method)
                param_docs = extract_param_docs(get_docstring(init_method))
                params_to_show = [
                    (name, param)
                    for name, param in sig.parameters.items()
                    if name != "self"
                ]

                # Add description outside callout
                if init_docstring:
                    docs.append(process_docstring(init_docstring))
                    docs.append("")

                # Add parameters in callout
                if params_to_show:
                    docs.append("**Parameters**")
                    for name, param in params_to_show:
                        param_doc = param_docs.get(name, "")
                        param_field = generate_param_field(name, param, param_doc)
                        docs.append(f">{param_field}")
                    docs.append("")

                # Constructor signature
                docs.append("**Signature**")
                docs.append("```python wrap")
                sig_str = format_signature(init_method)
                docs.append(f"def __init__{sig_str}:")
                docs.append("```")
                docs.append("")

                docs.append("</Card>")

            except Exception:
                docs.append("Error generating parameters")
                docs.append("")
                docs.append("</Card>")
                docs.append("")

    # Methods - only include methods defined in this module (including async methods and properties)
    for name, method in inspect.getmembers(
        cls,
        predicate=lambda x: inspect.isfunction(x)
        or inspect.iscoroutinefunction(x)
        or isinstance(x, property),
    ):
        # For properties, check if the getter function is defined in this module
        is_property_in_module = False
        if isinstance(method, property):
            if hasattr(method, "fget") and method.fget:
                is_property_in_module = is_defined_in_module(method.fget, module_name)
        else:
            is_property_in_module = is_defined_in_module(method, module_name)

        # Skip inherited methods/properties - only show those defined in this class
        is_inherited = False
        if isinstance(method, property):
            if hasattr(method, "fget") and method.fget:
                # Check if this property is defined in a parent class
                for base_cls in cls.__bases__:
                    if hasattr(base_cls, name) and isinstance(
                        getattr(base_cls, name), property
                    ):
                        is_inherited = True
                        break
        else:
            # Check if this method is defined in a parent class
            for base_cls in cls.__bases__:
                if hasattr(base_cls, name) and inspect.isfunction(
                    getattr(base_cls, name)
                ):
                    is_inherited = True
                    break

        if (
            (not name.startswith("_") or name in ["__call__", "__enter__", "__exit__"])
            and is_property_in_module
            and not is_inherited
        ):
            # Method/Property Card
            docs.append('<Card type="info">')
            if isinstance(method, property):
                docs.append(f"### `property` {name}")
            else:
                docs.append(f"### `method` {name}")
            docs.append("")

            method_docstring = get_docstring(method)

            # Handle properties differently from regular methods
            if isinstance(method, property):
                # Add description outside callout
                if method_docstring:
                    docs.append(process_docstring(method_docstring))
                    docs.append("")
            else:
                # Regular method handling
                sig = inspect.signature(method)
                param_docs = extract_param_docs(get_docstring(method))
                params_to_show = [
                    (name_param, param)
                    for name_param, param in sig.parameters.items()
                    if name_param != "self"
                ]

                # Add description outside callout
                if method_docstring:
                    docs.append(process_docstring(method_docstring))
                    docs.append("")

            docs.append("")

            # Handle properties differently from regular methods
            if isinstance(method, property):
                # Add return type for properties
                if hasattr(method, "fget") and method.fget:
                    sig = inspect.signature(method.fget)
                    return_annotation = sig.return_annotation
                    if return_annotation != inspect.Signature.empty:
                        docs.append("**Returns**")
                        docs.append(
                            f'><ResponseField name="returns" type="{format_type_annotation(return_annotation)}" />'
                        )
                        docs.append("")

                # Property signature
                docs.append("**Signature**")
                docs.append("```python wrap")
                docs.append(f"def {name}():")
                docs.append("```")
                docs.append("")
            else:
                # Add parameters in callout
                if params_to_show:
                    docs.append("**Parameters**")
                    for name_param, param in params_to_show:
                        param_doc = param_docs.get(name_param, "")
                        param_field = generate_param_field(name_param, param, param_doc)
                        docs.append(f">{param_field}")
                    docs.append("")

            # Generate return type and signature
            try:
                # Return type (skip for properties as they're handled separately)
                if (
                    not isinstance(method, property)
                    and sig.return_annotation != inspect.Signature.empty
                ):
                    return_type = format_type_annotation(sig.return_annotation)
                    return_desc = extract_return_docs(get_docstring(method))

                    # Only show Returns section if there's a meaningful return type (not None)
                    if return_type not in ["None", "NoneType"]:
                        docs.append("**Returns**")
                        # Use ResponseField for better formatting
                        response_field = generate_response_field(
                            "returns", return_type, return_desc
                        )
                        docs.append(f">{response_field}")
                        docs.append("")

                # Method signature (only for methods, not properties)
                if not isinstance(method, property):
                    docs.append("**Signature**")
                    docs.append("```python wrap")

                    sig_str = format_signature(method)
                    # Split long signatures across multiple lines
                    signature_line = f"def {name}{sig_str}:"
                    if len(signature_line) > 100:
                        # Split parameters for better readability
                        docs.append(f"def {name}(")
                        if sig_str != "()":
                            params = sig_str[1:-1].split(", ")
                            for i, param in enumerate(params):
                                prefix = "    " if i > 0 else ""
                                suffix = "," if i < len(params) - 1 else ""
                                docs.append(f"{prefix}{param}{suffix}")
                        docs.append("):")
                    else:
                        docs.append(signature_line)
                    docs.append("```")
                    docs.append("")
            except Exception:
                docs.append("Error generating parameters")
                docs.append("")

            docs.append("</Card>")
            docs.append("")

    # Close the main class Card
    docs.append("</div>")

    return "\n".join(docs)


def generate_function_docs(func: Any, module_name: str) -> str:
    """Generate documentation for a function."""
    docs = []

    func_name = func.__name__
    docstring = get_docstring(func)

    # Function section header for sidebar
    docs.append("")
    docs.append(f"## {func_name}")

    # Function Card
    docs.append('<Card type="info">')
    docs.append(f"### `function` {func_name}")
    docs.append("")

    # Function description
    if docstring:
        docs.append(process_docstring(docstring))
        docs.append("")

    # Add import example before parameters
    docs.append("```python")
    docs.append(f"from {module_name} import {func_name}")
    docs.append("```")
    docs.append("")

    # Parameters section
    sig = inspect.signature(func)
    param_docs = extract_param_docs(get_docstring(func))

    if sig.parameters:
        docs.append("**Parameters**")
        for name, param in sig.parameters.items():
            param_doc = param_docs.get(name, "")
            param_field = generate_param_field(name, param, param_doc)
            docs.append(f">{param_field}")
        docs.append("")

    # Returns section
    if sig.return_annotation != inspect.Signature.empty:
        return_type = format_type_annotation(sig.return_annotation)
        return_desc = extract_return_docs(get_docstring(func))

        # Only show Returns section if there's a meaningful return type (not None)
        if return_type not in ["None", "NoneType"]:
            docs.append("**Returns**")
            response_field = generate_response_field(
                "returns", return_type, return_desc
            )
            docs.append(f">{response_field}")
            docs.append("")

    # Warns section
    warns_docs = extract_warns_docs(get_docstring(func))
    if warns_docs:
        docs.append("**Warns**")
        for warn in warns_docs:
            docs.append(f"><Warning>{warn}</Warning>")
        docs.append("")

    # Raises section
    raises_docs = extract_raises_docs(get_docstring(func))
    if raises_docs:
        docs.append("**Raises**")
        for raise_desc in raises_docs:
            docs.append(f"><Danger>{raise_desc}</Danger>")
        docs.append("")

    # Signature section
    docs.append("**Signature**")
    docs.append("```python wrap")

    try:
        sig_str = format_signature(func)
        # Split long signatures across multiple lines
        signature_line = f"def {func_name}{sig_str}:"
        if len(signature_line) > 100:
            # Split parameters for better readability
            docs.append(f"def {func_name}(")
            if sig_str != "()":
                params = sig_str[1:-1].split(", ")
                for i, param in enumerate(params):
                    prefix = "    " if i > 0 else ""
                    suffix = "," if i < len(params) - 1 else ""
                    docs.append(f"{prefix}{param}{suffix}")
            docs.append("):")
        else:
            docs.append(signature_line)
    except Exception:
        docs.append(f"def {func_name}():")

    docs.append("```")
    docs.append("")
    docs.append("</Card>")

    return "\n".join(docs)


def generate_module_docs(module_name: str, output_dir: str) -> None:
    """Generate MDX documentation for a module."""
    try:
        module = __import__(module_name, fromlist=[""])
    except ImportError as e:
        print(f"Error importing module {module_name}: {e}")
        return

    # Detect deprecated items in the module
    module_path = module_name.replace(".", "/")
    source_file_path = f"../libraries/python/{module_path}.py"
    deprecated_items = detect_deprecated_items(source_file_path)

    # Get all members of the module
    members = inspect.getmembers(module)

    # Filter classes and functions (same logic as used later)
    classes = [
        (name, obj)
        for name, obj in members
        if inspect.isclass(obj)
        and not name.startswith("_")
        and is_defined_in_module(obj, module_name)
    ]
    functions = [
        (name, obj)
        for name, obj in members
        if inspect.isfunction(obj)
        and not name.startswith("_")
        and is_defined_in_module(obj, module_name)
    ]

    # Check if module only contains deprecated items
    if deprecated_items and (classes or functions):
        # Check if all classes and functions are deprecated
        all_classes_deprecated = all(name in deprecated_items for name, _ in classes)
        all_functions_deprecated = all(
            name in deprecated_items for name, _ in functions
        )

        if all_classes_deprecated and all_functions_deprecated:
            print(
                f"  ⚠️  Skipping {module_name} - module only contains deprecated items"
            )
            return

    # Create output directory if it doesn't exist
    os.makedirs(output_dir, exist_ok=True)

    # Generate filename preserving module path structure
    filename = f"{module_name.replace('.', '_')}.mdx"
    output_path = os.path.join(output_dir, filename)

    # Start building the MDX content
    content = []

    # Frontmatter
    title = module_name.split(".")[-1].replace("_", " ").title()
    module_docstring = get_docstring(module)

    # Get icon from module info
    module_info = get_module_info_from_filename(f"{module_name.replace('.', '_')}.mdx")
    icon = module_info["icon"]

    # Generate better description
    description = generate_module_description(module_name, module_docstring)

    # Generate GitHub URL for source code
    # Handle library-specific paths
    module_path = module_name.replace(".", "/")
    github_url = f"https://github.com/mcp-use/mcp-use/blob/main/libraries/python/{module_path}.py"

    frontmatter = {
        "title": title,
        "description": description,
        "icon": icon,
        "github": github_url,
    }

    content.append("---")
    for key, value in frontmatter.items():
        content.append(f'{key}: "{value}"')
    content.append("---")
    content.append("")
    content.append('import {RandomGradientBackground} from "/snippets/gradient.jsx"')
    content.append("")
    # GitHub source code callout
    content.append('<Callout type="info" title="Source Code">')
    content.append(
        f"View the source code for this module on GitHub: <a href='{github_url}'"
        f" target='_blank' rel='noopener noreferrer'>{github_url}</a>"
    )
    content.append("</Callout>")
    content.append("")

    # Module description
    if module_docstring:
        content.append(process_docstring(module_docstring))
        content.append("")

    # Filter out deprecated items from classes and functions
    filtered_classes = [
        (name, obj) for name, obj in classes if name not in deprecated_items
    ]
    for _, cls in filtered_classes:
        content.append(generate_class_docs(cls, module_name))
        content.append("")

    filtered_functions = [
        (name, obj) for name, obj in functions if name not in deprecated_items
    ]
    for _, func in filtered_functions:
        content.append(generate_function_docs(func, module_name))
        content.append("")

    # Write the file
    with open(output_path, "w", encoding="utf-8") as f:
        f.write("\n".join(content))

    print(f"Generated documentation for {module_name} -> {output_path}")


def find_python_modules(
    package_dir: str, exclude_patterns: list[str] | None = None
) -> list[str]:
    """Find all Python modules in a package directory, excluding __init__.py files.

    Args:
        package_dir: The package directory to search
        exclude_patterns: List of patterns to exclude (e.g., ['telemetry', 'internal'])
    """
    if exclude_patterns is None:
        exclude_patterns = []

    modules = []
    package_path = Path(package_dir)

    if not package_path.exists():
        return modules

    # Find all submodules, excluding __init__.py files
    for py_file in package_path.rglob("*.py"):
        if py_file.name == "__init__.py":
            # Skip __init__.py files
            continue
        elif not py_file.name.startswith("__"):
            # This is a module
            rel_path = py_file.relative_to(package_path.parent)
            module_name = str(rel_path.with_suffix("")).replace("/", ".")

            # Check if module matches any exclusion pattern
            should_exclude = any(pattern in module_name for pattern in exclude_patterns)

            if not should_exclude and module_name not in modules:
                modules.append(module_name)

    return sorted(modules)


def get_module_info_from_filename(filename: str) -> dict[str, str]:
    """Extract module information from filename."""
    # Remove .mdx extension
    name = filename.replace(".mdx", "")

    # Convert snake_case to Title Case
    display_name = name.replace("_", " ").title()

    # Map specific modules to icons with better semantic mapping
    icon_map = {
        # Core modules
        "mcpagent": "bot",
        "mcpclient": "router",
        "client": "router",
        "server": "server",
        "agent": "bot",
        # Adapters and connectors
        "adapters": "plug-2",
        "connectors": "cable",
        "base": "box",
        "http": "globe",
        "websocket": "wifi",
        "stdio": "terminal",
        "sandbox": "lock",
        "streamable_http": "globe",
        "sse": "radio",
        # Authentication
        "auth": "key",
        "bearer": "shield",
        "oauth": "key",
        "oauth_callback": "key",
        # Middleware and processing
        "middleware": "layers",
        "middleware_logging": "logs",
        "middleware_metrics": "bar-chart",
        # Observability and monitoring
        "observability": "eye",
        "telemetry": "chart-line",
        "logging": "logs",
        "callbacks_manager": "phone-callback",
        "laminar": "chart-bar",
        "langfuse": "chart-line",
        "metrics": "bar-chart",
        "events": "calendar",
        # Configuration and management
        "config": "settings",
        "session": "session",
        "managers": "users",
        "server_manager": "server-cog",
        "task_managers": "command",
        # Tools and utilities
        "tools": "hammer",
        "base_tool": "hammer",
        "connect_server": "server-plus",
        "disconnect_server": "server-minus",
        "get_active_server": "server",
        "list_servers_tool": "list",
        "search_tools": "search",
        "utils": "tool",
        # CLI and interfaces
        "cli": "terminal",
        # Types and data structures
        "types": "type",
        # Prompts and templates
        "prompts": "volume-2",
        "templates": "file-template",
        "system_prompt_builder": "file-text",
        # Error handling
        "exceptions": "triangle-alert",
        "errors": "triangle-alert",
        "error_formatting": "triangle-alert",
        # Remote and cloud
        "remote": "cloud",
        "openmcp": "server",
    }

    icon = icon_map.get(name, "code")

    return {"module": name, "display_name": display_name, "icon": icon}


def organize_modules_by_path(
    files: list[str],
) -> dict[str, dict[str, list[dict[str, str]]]]:
    """Organize modules by file path structure with nested groups."""
    packages = {}

    for filename in files:
        if filename.endswith(".mdx"):
            # Skip __init__.py files
            if filename == "__init__.mdx":
                continue

            module_info = get_module_info_from_filename(filename)

            # Extract module path from filename automatically
            module_path = filename.replace(".mdx", "")

            # Convert filename to module path automatically
            if module_path.startswith("mcp_use_"):
                # Remove mcp_use_ prefix and convert to module path
                remaining = module_path[8:]  # Remove 'mcp_use_'

                # Handle compound package names by mapping them back
                compound_mappings = {
                    "task_managers": "task_managers",
                    "error_formatting": "error_formatting",
                    "oauth_callback": "oauth_callback",
                    "callbacks_manager": "callbacks_manager",
                    "streamable_http": "streamable_http",
                    "langchain_adapter": "langchain_adapter",
                    "system_prompt_builder": "system_prompt_builder",
                    "server_manager": "server_manager",
                    "base_tool": "base_tool",
                    "connect_server": "connect_server",
                    "disconnect_server": "disconnect_server",
                    "get_active_server": "get_active_server",
                    "list_servers_tool": "list_servers_tool",
                    "search_tools": "search_tools",
                }

                # Check if this is a compound name we need to preserve
                module_path = "mcp_use."
                parts = remaining.split("_")

                # Reconstruct the path, preserving compound names
                i = 0
                while i < len(parts):
                    # Check for compound names starting at this position
                    compound_found = False
                    for compound, replacement in compound_mappings.items():
                        compound_parts = compound.split("_")
                        if i + len(compound_parts) <= len(parts):
                            if "_".join(parts[i : i + len(compound_parts)]) == compound:
                                module_path += replacement + "."
                                i += len(compound_parts)
                                compound_found = True
                                break

                    if not compound_found:
                        module_path += parts[i] + "."
                        i += 1

                # Remove trailing dot
                module_path = module_path.rstrip(".")
            else:
                # Handle other cases
                module_path = module_path.replace("_", ".")

            # Dynamically determine package and subpackage based on module path
            path_parts = module_path.split(".")

            if len(path_parts) == 1:
                # Root level module (e.g., 'mcp_use')
                package = "core"
                subpackage = "root"
            elif len(path_parts) == 2:
                # Top-level package (e.g., 'mcp_use.client') - these are root-level modules
                package = "core"
                subpackage = path_parts[1]  # Use the module name as subpackage
            elif len(path_parts) >= 3:
                # Subpackage (e.g., 'mcp_use.task_managers.base')
                # The package is the compound name (e.g., 'task_managers')
                package = path_parts[1]
                subpackage = path_parts[2]
            else:
                # Fallback
                package = "other"
                subpackage = "root"

            if package not in packages:
                packages[package] = {}

            if subpackage:
                if subpackage not in packages[package]:
                    packages[package][subpackage] = []
                packages[package][subpackage].append(module_info)
            else:
                if "root" not in packages[package]:
                    packages[package]["root"] = []
                packages[package]["root"].append(module_info)

    return packages


def get_package_display_info(package: str) -> dict[str, str]:
    """Get display information for a package."""
    # Common package icons
    package_icons = {
        "core": "package",
        "client": "router",
        "server": "server",
        "agent": "bot",
        "auth": "key",
        "middleware": "layers",
        "connectors": "cable",
        "adapters": "plug-2",
        "observability": "eye",
        "telemetry": "chart-line",
        "logging": "logs",
        "cli": "terminal",
        "utils": "tool",
        "config": "settings",
        "session": "database",
        "errors": "triangle-alert",
        "types": "type",
        "task_managers": "command",
        "managers": "users",
        "prompts": "volume-2",
        "remote": "cloud",
        "sandbox": "shield",
        "http": "globe",
        "stdio": "terminal",
        "websocket": "wifi",
        "bearer": "key",
        "oauth": "lock",
        "oauth_callback": "refresh-cw",
        "error_formatting": "alert-triangle",
        "exceptions": "alert-circle",
        "callbacks_manager": "users",
        "laminar": "layers",
        "langfuse": "chart-line",
        "base": "layers",
        "sse": "activity",
        "streamable_http": "globe",
        "events": "calendar",
        "other": "code",
    }

    # Use predefined icon if available, otherwise default to 'code'
    icon = package_icons.get(package, "code")

    # Convert package name to display name
    display_name = package.replace("_", " ").title()

    return {"name": display_name, "icon": icon}


def get_subpackage_display_info(subpackage: str) -> dict[str, str]:
    """Get display information for a subpackage."""
    # Common subpackage icons
    subpackage_icons = {
        "root": "globe",
        "managers": "users",
        "adapters": "plug-2",
        "observability": "eye",
        "prompts": "volume-2",
        "auth": "key",
        "connectors": "cable",
        "middleware": "layers",
        "task_managers": "tasks",
        "errors": "alert-triangle",
        "types": "type",
        "tools": "tool",
        "remote": "cloud",
        "sandbox": "shield",
        "http": "globe",
        "stdio": "terminal",
        "websocket": "wifi",
        "bearer": "key",
        "oauth": "lock",
        "oauth_callback": "refresh-cw",
        "error_formatting": "alert-triangle",
        "exceptions": "alert-circle",
        "callbacks_manager": "users",
        "laminar": "layers",
        "langfuse": "chart-line",
        "base": "layers",
        "sse": "activity",
        "streamable_http": "globe",
        "events": "calendar",
        "telemetry": "chart-line",
        "utils": "tool",
        "config": "settings",
        "session": "database",
        "logging": "logs",
        "cli": "terminal",
    }

    # Use predefined icon if available, otherwise default to 'code'
    icon = subpackage_icons.get(subpackage, "code")

    # Convert subpackage name to display name
    if subpackage == "root":
        display_name = "Overview"
    else:
        display_name = subpackage.replace("_", " ").title()

    return {"name": display_name, "icon": icon}


def generate_api_reference_groups(
    packages: dict[str, dict[str, list[dict[str, str]]]],
) -> list[dict[str, Any]]:
    """Generate API reference groups from organized packages with nested structure."""
    groups = []

    # Process packages in alphabetical order for consistent organization
    for package in sorted(packages.keys()):
        if package in packages and packages[package]:
            package_info = get_package_display_info(package)

            # Check if this package has subpackages
            has_subpackages = any(key != "root" for key in packages[package].keys())

            if has_subpackages:
                # Create nested structure with subpackages
                subpackages = []

                # Add root items directly to subpackages list (no Overview section)
                if "root" in packages[package] and packages[package]["root"]:
                    root_modules = sorted(
                        packages[package]["root"], key=lambda x: x["display_name"]
                    )
                    root_pages = [
                        f"python/api-reference/{module['module']}"
                        for module in root_modules
                    ]
                    subpackages.extend(root_pages)

                # Add other subpackages - only create subsections if they have more than 1 item
                for subpackage_name in sorted(packages[package].keys()):
                    if subpackage_name == "root":
                        continue

                    modules = packages[package][subpackage_name]

                    # Only create subsection if there's more than 1 module
                    if len(modules) > 1:
                        subpackage_info = get_subpackage_display_info(subpackage_name)
                        sorted_modules = sorted(
                            modules, key=lambda x: x["display_name"]
                        )
                        pages = [
                            f"python/api-reference/{module['module']}"
                            for module in sorted_modules
                        ]

                        subpackages.append(
                            {
                                "group": subpackage_info["name"],
                                "icon": subpackage_info["icon"],
                                "pages": pages,
                            }
                        )
                    else:
                        # Single module - add directly to root level
                        module = modules[0]
                        subpackages.append(f"python/api-reference/{module['module']}")

                group = {
                    "group": package_info["name"],
                    "icon": package_info["icon"],
                    "pages": subpackages,
                }
            else:
                # Simple structure without subpackages
                modules = sorted(
                    packages[package]["root"], key=lambda x: x["display_name"]
                )
                pages = [
                    f"python/api-reference/{module['module']}" for module in modules
                ]

                group = {
                    "group": package_info["name"],
                    "icon": package_info["icon"],
                    "pages": pages,
                }

            groups.append(group)

    return groups


def scan_all_files_for_deprecated_items(package_dir: str) -> dict[str, list[str]]:
    """Scan all Python files in a package for deprecated items.

    Args:
        package_dir: The package directory to scan

    Returns:
        Dictionary mapping file paths to lists of deprecated item names
    """
    deprecated_files = {}
    package_path = Path(package_dir)

    if not package_path.exists():
        return deprecated_files

    # Find all Python files
    for py_file in package_path.rglob("*.py"):
        if py_file.name == "__init__.py":
            continue

        deprecated_items = detect_deprecated_items(str(py_file))
        if deprecated_items:
            deprecated_files[str(py_file)] = deprecated_items

    return deprecated_files


def update_docs_json(docs_json_path: str, api_reference_dir: str) -> None:
    """Update docs.json with new API reference structure."""
    # Read existing docs.json as text to preserve formatting
    with open(docs_json_path, encoding="utf-8") as f:
        original_content = f.read()

    # Parse JSON for updates
    docs_config = json.loads(original_content)

    # Get all MDX files in api-reference directory
    api_ref_path = Path(api_reference_dir)
    if not api_ref_path.exists():
        print(f"API reference directory {api_reference_dir} does not exist")
        return

    mdx_files = [f.name for f in api_ref_path.glob("*.mdx")]

    # Organize modules by path
    packages = organize_modules_by_path(mdx_files)

    # Generate API reference groups
    api_groups = generate_api_reference_groups(packages)

    # Update the navigation structure
    # Find the Python SDK product and then the API Reference tab within it.
    for product in docs_config["navigation"]["products"]:
        if product.get("product") == "Python SDK":
            if "tabs" in product:
                for tab in product["tabs"]:
                    if "API Reference" in tab.get("tab", ""):
                        tab["groups"] = api_groups
                        # Keep the auto-generated API reference visible in the
                        # sidebar but out of docs search, so generic queries
                        # (e.g. "CLI") surface curated guides instead of the
                        # generated reference. See MCP-2398.
                        tab["searchable"] = False
                        break  # Found the tab, stop searching tabs
            break  # Found the product, stop searching products

    # Write updated docs.json
    with open(docs_json_path, "w", encoding="utf-8") as f:
        json.dump(docs_config, f, indent=2, ensure_ascii=False)
        f.write("\n")

    print(f"Updated {docs_json_path} with {len(mdx_files)} API reference files")
    print(f"Organized into {len(api_groups)} groups")


def main():
    """Main function."""
    if len(sys.argv) < 2:
        print(
            "Usage: python generate_docs.py <package_dir> [output_dir] [docs_json_path] [--scan-only]"
        )
        print(
            "Example: python generate_docs.py ../libraries/python/mcp_use python/api-reference docs.json"
        )
        print(
            "Example: python generate_docs.py ../libraries/python/mcp_use --scan-only"
        )
        sys.exit(1)

    package_dir = sys.argv[1]
    output_dir = sys.argv[2] if len(sys.argv) > 2 else "api-reference"
    docs_json_path = sys.argv[3] if len(sys.argv) > 3 else "docs.json"
    scan_only = "--scan-only" in sys.argv

    # If scan-only mode, just scan for deprecated items and exit
    if scan_only:
        print("🔍 Scanning for deprecated items...")
        deprecated_files = scan_all_files_for_deprecated_items(package_dir)

        if not deprecated_files:
            print("✅ No deprecated items found!")
            return

        print(f"⚠️  Found deprecated items in {len(deprecated_files)} files:")
        print()

        for file_path, deprecated_items in deprecated_files.items():
            print(f"📁 {file_path}")
            for item in deprecated_items:
                print(f"   - {item}")
        print()

        return

    # Add the directory containing the package to Python path
    # Convert package_dir (e.g., ../libraries/python/mcp_use) to absolute path
    package_abs_path = os.path.abspath(package_dir)
    # Get the parent directory that contains the package
    parent_dir = os.path.dirname(package_abs_path)
    sys.path.insert(0, parent_dir)

    print("🔄 Generating API documentation...")

    # Step 1: Generate all API docs
    # Exclude telemetry and other private modules
    exclude_patterns = ["telemetry"]
    modules = find_python_modules(package_dir, exclude_patterns)
    print(f"Found {len(modules)} modules (excluding: {', '.join(exclude_patterns)})")

    # Step 1a: Remove stale documentation files
    print("🔄 Checking for stale documentation files...")
    existing_mdx_files = (
        list(Path(output_dir).glob("*.mdx")) if os.path.exists(output_dir) else []
    )
    existing_module_names = {f.stem for f in existing_mdx_files}
    expected_module_names = {module.replace(".", "_") for module in modules}

    stale_files = existing_module_names - expected_module_names
    if stale_files:
        print(f"🗑️  Found {len(stale_files)} stale documentation file(s) to remove:")
        for stale_name in sorted(stale_files):
            stale_path = Path(output_dir) / f"{stale_name}.mdx"
            if stale_path.exists():
                stale_path.unlink()
                print(f"   - Removed {stale_path}")
    else:
        print("✅ No stale documentation files found")

    # Generate docs for each module
    success_count = 0
    for module in modules:
        try:
            print(f"Generating docs for {module}...")

            # Check for deprecated items in the module
            module_path = module.replace(".", "/")
            source_file_path = f"../libraries/python/{module_path}.py"
            deprecated_items = detect_deprecated_items(source_file_path)
            if deprecated_items:
                print(
                    f"  ⚠️  Found {len(deprecated_items)} deprecated item(s) in {module}: {', '.join(deprecated_items)}"
                )

            generate_module_docs(module, output_dir)
            success_count += 1
        except Exception as e:
            print(f"Error generating docs for {module}: {e}")
            continue

    print(f"✅ Generated documentation for {success_count}/{len(modules)} modules")

    # Step 2: Update docs.json
    print("🔄 Updating docs.json...")

    if os.path.exists(docs_json_path):
        update_docs_json(docs_json_path, output_dir)
        print("✅ Updated docs.json with new API reference structure")
    else:
        print(f"⚠️  docs.json not found at {docs_json_path}, skipping update")

    print("🎉 API documentation generation complete!")
    print(f"📁 Files generated in: {output_dir}")
    print(f"📄 Navigation updated in: {docs_json_path}")


if __name__ == "__main__":
    main()
