"""MCP tools for managing individual Portal API entries."""
import json
from urllib.parse import urlparse

from mcp.types import TextContent, Tool

from maeris_mcp.auth.token_store import TokenStore
from maeris_mcp.maeris.client import (
    AuthenticationError,
    MaerisClient,
    MaerisClientError,
    RateLimitError,
)
from maeris_mcp.maeris.types import MaerisAPI
from maeris_mcp.tools.collection_tools import _find_collection_by_name, _find_collection_by_path, _get_children


# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------

async def _find_api_in_collection(
    client: MaerisClient, collection_name: str, api_name: str
) -> tuple[dict, str] | None:
    """Find an API by exact name (case-insensitive) within a named collection.

    Returns (api_dict, collection_id) or None.
    """
    collection = await _find_collection_by_name(client, collection_name)
    if not collection:
        return None
    collection_id = collection["api_collection_id"]
    children = await _get_children(client, collection_id)
    for api in children.get("apis", []):
        if api["name"].lower() == api_name.lower():
            return api, collection_id
    return None


def _parameterize_url(url: str) -> str:
    """Convert a raw URL/path to use ``{{base_url}}`` parameterization.

    Examples:
      /api/users              → {{base_url}}/api/users
      http://host:3000/api/x  → {{base_url}}/api/x
      {{base_url}}/api/x      → {{base_url}}/api/x  (unchanged)
    """
    if "{{base_url}}" in url:
        return url

    if url.startswith(("http://", "https://")):
        parsed = urlparse(url)
        path = parsed.path or "/"
        if parsed.query:
            path += f"?{parsed.query}"
        return f"{{{{base_url}}}}{path}"

    if url.startswith("/"):
        return f"{{{{base_url}}}}{url}"

    return f"{{{{base_url}}}}/{url}"


def _extract_base_url_from_apis(apis: list[dict]) -> str | None:
    """Try to extract a base URL from the first API that has a full URL."""
    for api in apis:
        url = api.get("url", "")
        if url.startswith(("http://", "https://")):
            parsed = urlparse(url)
            return f"{parsed.scheme}://{parsed.netloc}"
    return None


def _check_auth() -> str | None:
    """Return an error message if the user is not authenticated, else None."""
    token_store = TokenStore()
    if not token_store.load_token():
        return "Error: not authenticated. Run 'maeris login' first."
    if token_store.is_token_expired():
        return "Error: your session has expired. Run 'maeris login' to re-authenticate."
    stored = token_store.get_token_data() or {}
    if not stored.get("app_id"):
        return "Error: no application selected. Run 'maeris switch-app' to link an application."
    return None


def _format_client_error(exc: Exception) -> str:
    """Return a user-friendly error message for MaerisClient exceptions."""
    if isinstance(exc, AuthenticationError):
        return f"Error: authentication failed — {exc}. Run 'maeris login' to re-authenticate."
    if isinstance(exc, RateLimitError):
        return "Error: rate limited by the Maeris API. Please wait a moment and try again."
    if isinstance(exc, MaerisClientError):
        return f"Error: {exc}"
    return f"Error: unexpected failure — {exc}"


async def _find_api_by_name(
    client: MaerisClient, name: str
) -> tuple[dict | None, str | None, str | None]:
    """Find an API by name across all APIs.

    Returns (matched_api, api_id, warning_or_none).
    The warning is set when the match was fuzzy (substring) rather than exact.
    Returns (None, None, error_message) when no match is found.
    """
    all_apis = await client.get_all_apis()
    exact = [a for a in all_apis if (a.get("name") or "").lower() == name.lower()]
    if exact:
        api = exact[0]
        return api, api.get("api_id") or api.get("id"), None

    fuzzy = [a for a in all_apis if name.lower() in (a.get("name") or "").lower()]
    if not fuzzy:
        available = [a.get("name") for a in all_apis[:10]]
        hint = f" Available APIs (first 10): {available}" if available else ""
        return None, None, f"Error: API '{name}' not found.{hint}"

    api = fuzzy[0]
    api_id = api.get("api_id") or api.get("id")
    warning = None
    if len(fuzzy) > 1:
        names = [a.get("name") for a in fuzzy]
        warning = f"Note: no exact match for '{name}' — matched '{api.get('name')}'. Other partial matches: {names[1:]}"
    else:
        warning = f"Note: no exact match for '{name}' — using closest match '{api.get('name')}'."
    return api, api_id, warning


def _env_id(env: dict) -> str:
    for key in ("environment_id", "api_environment_id", "id", "env_id"):
        if v := env.get(key):
            return str(v)
    return ""


def _env_name(env: dict) -> str:
    for key in ("name", "environment_name"):
        v = env.get(key)
        if isinstance(v, str) and v.strip():
            return v
    return ""


# ---------------------------------------------------------------------------
# Tool definitions
# ---------------------------------------------------------------------------

def add_api_tool() -> Tool:
    return Tool(
        name="add_api",
        description=(
            "Create a new API entry and add it to a named collection. "
            "Optionally attach headers and query params."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Display name for the API."},
                "method": {"type": "string", "description": "HTTP method (GET, POST, PUT, DELETE, PATCH, etc.)."},
                "url": {"type": "string", "description": "Full URL for the API endpoint."},
                "collection_name": {"type": "string", "description": "Name of the collection to add this API to."},
                "payload": {"description": "Optional request body / payload."},
                "headers": {
                    "type": "array",
                    "description": "Optional list of headers.",
                    "items": {
                        "type": "object",
                        "properties": {
                            "field_name": {"type": "string"},
                            "field_value": {"type": "string"},
                        },
                        "required": ["field_name", "field_value"],
                    },
                },
                "params": {
                    "type": "array",
                    "description": "Optional list of query parameters.",
                    "items": {
                        "type": "object",
                        "properties": {
                            "field_name": {"type": "string"},
                            "field_value": {"type": "string"},
                        },
                        "required": ["field_name", "field_value"],
                    },
                },
            },
            "required": ["name", "method", "url", "collection_name"],
        },
    )


def add_apis_bulk_tool() -> Tool:
    return Tool(
        name="add_apis_bulk",
        description=(
            "Create multiple API entries and add them all to a named collection in one call. "
            "When auto_environment is true, automatically checks for an existing environment "
            "(by project_name), creates one if missing, populates it with extracted fields "
            "(base_url, auth config, etc.), parameterizes all API URLs as {{base_url}}/path, "
            "and selects the environment."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "collection_name": {"type": "string", "description": "Name of the collection to add the APIs to."},
                "apis": {
                    "type": "array",
                    "description": "List of API definitions to create.",
                    "items": {
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "method": {"type": "string"},
                            "url": {"type": "string"},
                            "payload": {},
                            "headers": {"type": "array", "items": {"type": "object"}},
                            "params": {"type": "array", "items": {"type": "object"}},
                        },
                        "required": ["name", "method", "url"],
                    },
                },
                "auto_environment": {
                    "type": "boolean",
                    "description": (
                        "When true, automatically set up an environment for these APIs. "
                        "Checks for an existing environment matching project_name, creates one if needed, "
                        "populates it with extracted_environment fields, parameterizes API URLs as "
                        "{{base_url}}/path, and selects the environment. Default: false."
                    ),
                    "default": False,
                },
                "project_name": {
                    "type": "string",
                    "description": "Project/environment name. Used to find or create the environment. Defaults to collection_name.",
                },
                "extracted_environment": {
                    "type": "array",
                    "description": (
                        "Environment fields extracted from the scan (e.g. base_url, auth_token_source). "
                        "Pass the extractedEnvironment array from the api_scan finalization response."
                    ),
                    "items": {
                        "type": "object",
                        "properties": {
                            "field_name": {"type": "string"},
                            "field_value": {"type": "string"},
                        },
                        "required": ["field_name", "field_value"],
                    },
                },
            },
            "required": ["collection_name", "apis"],
        },
    )


def get_api_tool() -> Tool:
    return Tool(
        name="get_api",
        description="Get full details of a single API (including headers and params) by name.",
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name of the API to retrieve."},
            },
            "required": ["name"],
        },
    )


def list_apis_tool() -> Tool:
    return Tool(
        name="list_apis",
        description="List all API entries for the connected Maeris app.",
        inputSchema={
            "type": "object",
            "properties": {},
            "required": [],
        },
    )


def update_api_tool() -> Tool:
    return Tool(
        name="update_api",
        description="Update an existing API's name, URL, method, or payload. At least one optional field is required.",
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Current name of the API to update."},
                "new_name": {"type": "string", "description": "New display name."},
                "new_url": {"type": "string", "description": "New URL."},
                "new_method": {"type": "string", "description": "New HTTP method."},
                "new_payload": {"description": "New request payload."},
            },
            "required": ["name"],
        },
    )


def delete_api_tool() -> Tool:
    return Tool(
        name="delete_api",
        description="Delete an API entry by name and collection. This is irreversible.",
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name of the API to delete."},
                "collection_name": {"type": "string", "description": "Name of the collection that contains the API."},
            },
            "required": ["name", "collection_name"],
        },
    )


def move_api_tool() -> Tool:
    return Tool(
        name="move_api",
        description=(
            "Move an API from one collection to another. "
            "If source and target are the same, this is treated as a successful no-op."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name of the API to move."},
                "from_collection": {"type": "string", "description": "Name of the source collection."},
                "to_collection": {"type": "string", "description": "Name of the target collection."},
            },
            "required": ["name", "from_collection", "to_collection"],
        },
    )


def duplicate_api_tool() -> Tool:
    return Tool(
        name="duplicate_api",
        description=(
            "Duplicate an existing API, copying its definition and headers/params. "
            "Optionally place the copy in a different collection with a different name."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name of the API to duplicate."},
                "target_collection": {"type": "string", "description": "Collection to place the copy in. Use '>' to specify a path for disambiguation, e.g. 'API > API'."},
                "new_name": {"type": "string", "description": "Name for the copy. Defaults to 'Copy of {name}'."},
            },
            "required": ["name"],
        },
    )


# ---------------------------------------------------------------------------
# Handlers
# ---------------------------------------------------------------------------

async def handle_add_api(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    method = arguments.get("method")
    url = arguments.get("url")
    collection_name = arguments.get("collection_name")

    if not all([name, method, url, collection_name]):
        return [TextContent(type="text", text="Error: name, method, url, and collection_name are all required.")]

    headers = arguments.get("headers") or []
    params = arguments.get("params") or []
    payload = arguments.get("payload", "")

    try:
        async with MaerisClient() as client:
            collection = await _find_collection_by_name(client, collection_name)
            if not collection:
                return [TextContent(type="text", text=f"Error: no collection found with name '{collection_name}'. Use list_collections to see available collections, or create_collection to make a new one.")]
            collection_id = collection["api_collection_id"]

            api = MaerisAPI(name=name, type=method.lower(), url=url, payload=payload,
                            headers=headers, params=params, parent_collection_id=collection_id)
            api_id = await client.create_api(api)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    result = {
        "status": "created",
        "id": api_id,
        "name": name,
        "method": method.upper(),
        "url": url,
        "collectionId": collection_id,
        "collectionName": collection_name,
        "message": f"API '{name}' ({method.upper()} {url}) added to collection '{collection_name}'. Use add_api_param_headers to attach headers or query params.",
    }
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_add_apis_bulk(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    collection_name = arguments.get("collection_name")
    apis_input = arguments.get("apis") or []
    auto_environment = arguments.get("auto_environment", False)
    project_name = arguments.get("project_name") or collection_name
    extracted_environment = arguments.get("extracted_environment") or []

    if not collection_name:
        return [TextContent(type="text", text="Error: collection_name is required.")]
    if not apis_input:
        return [TextContent(type="text", text="Error: apis list is required and must not be empty.")]

    added = 0
    failed = 0
    created_apis = []
    errors = []
    env_result = None

    try:
        async with MaerisClient() as client:
            collection = await _find_collection_by_name(client, collection_name)
            if not collection:
                return [TextContent(type="text", text=f"Error: no collection found with name '{collection_name}'. Use list_collections to see available collections, or create_collection to make a new one.")]
            collection_id = collection["api_collection_id"]

            # Auto-environment setup
            if auto_environment:
                try:
                    env_result = await _setup_environment(
                        client, project_name, extracted_environment, apis_input,
                    )
                except Exception as exc:
                    errors.append(f"Environment setup failed (APIs will still be created without parameterized URLs): {exc}")

            for entry in apis_input:
                entry_name = entry.get("name")
                entry_method = entry.get("method")
                entry_url = entry.get("url")
                if not all([entry_name, entry_method, entry_url]):
                    failed += 1
                    errors.append(f"Skipped entry missing required fields (name/method/url): {json.dumps(entry)}")
                    continue

                # Parameterize URL when auto_environment is active
                if auto_environment:
                    entry_url = _parameterize_url(entry_url)

                try:
                    api = MaerisAPI(
                        name=entry_name,
                        type=entry_method.lower(),
                        url=entry_url,
                        payload=entry.get("payload", ""),
                        headers=entry.get("headers") or [],
                        params=entry.get("params") or [],
                        parent_collection_id=collection_id,
                    )
                    api_id = await client.create_api(api)
                    added += 1
                    created_apis.append({"id": api_id, "name": entry_name, "method": entry_method.upper(), "url": entry_url})
                except MaerisClientError as exc:
                    failed += 1
                    errors.append(f"Failed to create '{entry_name}': {exc}")
    except AuthenticationError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    message_parts = [f"{added} API(s) added to collection '{collection_name}'."]
    if failed:
        message_parts.append(f"{failed} failed — see errors for details.")
    if env_result:
        if env_result.get("created"):
            message_parts.append(f"Environment '{env_result['environmentName']}' created and selected.")
        else:
            message_parts.append(f"Environment '{env_result['environmentName']}' found and selected.")
        if env_result.get("base_url"):
            message_parts.append(f"Base URL: {env_result['base_url']}")

    result: dict = {
        "status": "completed",
        "added": added,
        "failed": failed,
        "apis": created_apis,
        "errors": errors,
        "message": " ".join(message_parts),
    }
    if env_result:
        result["environment"] = env_result
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_get_api(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    if not name:
        return [TextContent(type="text", text="Error: name is required.")]

    try:
        async with MaerisClient() as client:
            matched, api_id, warning = await _find_api_by_name(client, name)
            if not matched:
                return [TextContent(type="text", text=warning)]
            detail = await client.get_api(api_id)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    api_data = detail.get("api", detail)
    param_headers = detail.get("param_headers", [])
    headers = [ph for ph in param_headers if (ph.get("type") or "").upper() == "HEADERS"]
    params = [ph for ph in param_headers if (ph.get("type") or "").upper() == "PARAMS"]

    result = {
        "id": api_id,
        "name": api_data.get("name", name),
        "method": (api_data.get("type") or api_data.get("method", "")).upper(),
        "url": api_data.get("url", ""),
        "payload": api_data.get("payload", ""),
        "verification": api_data.get("verification", ""),
        "headers": headers,
        "params": params,
        "createdAt": api_data.get("created_at"),
    }
    if warning:
        result["warning"] = warning
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_list_apis(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    try:
        async with MaerisClient() as client:
            apis = await client.get_all_apis()
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    if not apis:
        return [TextContent(type="text", text=json.dumps({"apis": [], "message": "No APIs found. Use add_api or add_apis_bulk to create API entries."}, indent=2))]

    result = {
        "count": len(apis),
        "apis": [
            {
                "id": a.get("api_id") or a.get("id"),
                "name": a.get("name"),
                "method": (a.get("type") or a.get("method", "")).upper(),
                "url": a.get("url"),
                "createdAt": a.get("created_at"),
            }
            for a in apis
        ],
    }
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_update_api(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    if not name:
        return [TextContent(type="text", text="Error: name is required.")]

    update_payload: dict = {}
    if "new_name" in arguments:
        update_payload["name"] = arguments["new_name"]
    if "new_url" in arguments:
        update_payload["url"] = arguments["new_url"]
    if "new_method" in arguments:
        update_payload["type"] = arguments["new_method"].upper()
    if "new_payload" in arguments:
        update_payload["payload"] = arguments["new_payload"]

    if not update_payload:
        return [TextContent(type="text", text="Error: at least one of new_name, new_url, new_method, or new_payload is required.")]

    try:
        async with MaerisClient() as client:
            matched, api_id, warning = await _find_api_by_name(client, name)
            if not matched:
                return [TextContent(type="text", text=warning)]

            await client.update_api(api_id, update_payload)
            detail = await client.get_api(api_id)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    api_data = detail.get("api", detail)
    fields_changed = list(update_payload.keys())
    result = {
        "status": "updated",
        "id": api_id,
        "name": api_data.get("name", name),
        "method": (api_data.get("type") or api_data.get("method", "")).upper(),
        "url": api_data.get("url", ""),
        "fieldsUpdated": fields_changed,
        "message": f"API '{name}' updated successfully. Changed: {', '.join(fields_changed)}.",
    }
    if warning:
        result["warning"] = warning
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_delete_api(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    collection_name = arguments.get("collection_name")

    if not name or not collection_name:
        return [TextContent(type="text", text="Error: both name and collection_name are required.")]

    try:
        async with MaerisClient() as client:
            found = await _find_api_in_collection(client, collection_name, name)
            if not found:
                return [TextContent(type="text", text=f"Error: API '{name}' not found in collection '{collection_name}'. Use list_apis_in_collection to see APIs in that collection.")]
            api_dict, _collection_id = found
            api_id = api_dict["api_id"]

            await client.delete_api(api_id)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    result = {
        "status": "deleted",
        "id": api_id,
        "name": name,
        "collection": collection_name,
        "message": f"API '{name}' has been permanently deleted from collection '{collection_name}'.",
    }
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_move_api(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    from_collection = arguments.get("from_collection")
    to_collection = arguments.get("to_collection")

    if not all([name, from_collection, to_collection]):
        return [TextContent(type="text", text="Error: name, from_collection, and to_collection are all required.")]

    try:
        async with MaerisClient() as client:
            found = await _find_api_in_collection(client, from_collection, name)
            if not found:
                return [TextContent(type="text", text=f"Error: API '{name}' not found in collection '{from_collection}'. Use list_apis_in_collection to verify the API exists there.")]
            api_dict, source_collection_id = found
            api_id = api_dict["api_id"]

            target = await _find_collection_by_name(client, to_collection)
            if not target:
                return [TextContent(type="text", text=f"Error: target collection '{to_collection}' not found. Use list_collections to see available collections.")]
            target_collection_id = target["api_collection_id"]

            if source_collection_id == target_collection_id:
                result = {
                    "status": "no_change",
                    "id": api_id,
                    "name": name,
                    "fromCollection": from_collection,
                    "toCollection": to_collection,
                    "moved": False,
                    "message": f"API '{name}' is already in '{to_collection}' — no move needed.",
                }
                return [TextContent(type="text", text=json.dumps(result, indent=2))]

            await client.remove_api_from_collection(source_collection_id, api_id)
            await client.move_api_to_collection(target_collection_id, api_id)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    result = {
        "status": "moved",
        "id": api_id,
        "name": name,
        "fromCollection": from_collection,
        "toCollection": to_collection,
        "moved": True,
        "message": f"API '{name}' moved from '{from_collection}' to '{to_collection}'.",
    }
    return [TextContent(type="text", text=json.dumps(result, indent=2))]

async def handle_duplicate_api(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    if not name:
        return [TextContent(type="text", text="Error: name is required.")]

    new_name = arguments.get("new_name") or f"Copy of {name}"
    target_collection_name = arguments.get("target_collection")

    if not target_collection_name:
        return [TextContent(type="text", text="Error: target_collection is required. Specify which collection the copy should be placed in.")]

    try:
        async with MaerisClient() as client:
            matched, source_api_id, warning = await _find_api_by_name(client, name)
            if not matched:
                return [TextContent(type="text", text=warning)]

            detail = await client.get_api(source_api_id)
            api_data = detail.get("api", detail)
            param_headers = detail.get("param_headers", [])

            if ">" in target_collection_name:
                target = await _find_collection_by_path(client, target_collection_name)
            else:
                target = await _find_collection_by_name(client, target_collection_name)
            if not target:
                return [TextContent(type="text", text=f"Error: collection '{target_collection_name}' not found. Use list_collections to see available collections.")]
            target_collection_id = target["api_collection_id"]

            new_api = MaerisAPI(
                name=new_name,
                type=api_data.get("type") or api_data.get("method", "get"),
                url=api_data.get("url", ""),
                payload=api_data.get("payload", ""),
                verification=api_data.get("verification", ""),
                parent_collection_id=target_collection_id,
            )
            new_api_id = await client.create_api(new_api)

            headers_copied = 0
            params_copied = 0
            if param_headers:
                copy_headers = [ph for ph in param_headers if (ph.get("type") or "").upper() == "HEADERS"]
                copy_params = [ph for ph in param_headers if (ph.get("type") or "").upper() == "PARAMS"]
                if copy_headers or copy_params:
                    h = [{"field_name": ph["field_name"], "field_value": ph.get("field_value", "")} for ph in copy_headers]
                    p = [{"field_name": ph["field_name"], "field_value": ph.get("field_value", "")} for ph in copy_params]
                    await client.add_param_header(new_api_id, h, p)
                    headers_copied = len(h)
                    params_copied = len(p)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    parts = [f"API duplicated as '{new_name}' in collection '{target_collection_name}'."]
    if headers_copied or params_copied:
        parts.append(f"Copied {headers_copied} header(s) and {params_copied} param(s).")

    result = {
        "status": "duplicated",
        "original": {"id": source_api_id, "name": name},
        "copy": {"id": new_api_id, "name": new_name},
        "targetCollection": target_collection_name,
        "message": " ".join(parts),
    }
    if warning:
        result["warning"] = warning
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


# ---------------------------------------------------------------------------
# Helper: collect all APIs in a collection subtree (BFS)
# ---------------------------------------------------------------------------

async def _collect_all_apis_in_subtree(
    client: MaerisClient, collection_id: str, collection_name: str
) -> list[dict]:
    """Returns list of {id, name, method, url, collectionId, collectionName, collectionPath}."""
    results = []
    queue = [(collection_id, collection_name, collection_name)]
    while queue:
        col_id, col_name, path = queue.pop(0)
        children = await _get_children(client, col_id)
        for api in children.get("apis", []):
            results.append({
                "id": api["api_id"],
                "name": api["name"],
                "method": api["method"].upper(),
                "url": api["url"],
                "collectionId": col_id,
                "collectionName": col_name,
                "collectionPath": path,
            })
        for sub in children.get("api_collections", []):
            queue.append((sub["api_collection_id"], sub["name"], f"{path} / {sub['name']}"))
    return results


# ---------------------------------------------------------------------------
# Tool definitions (new)
# ---------------------------------------------------------------------------

def add_api_param_headers_tool() -> Tool:
    return Tool(
        name="add_api_param_headers",
        description=(
            "Add headers and/or query params to an existing API. "
            "Use this after creating an API when you need to attach new fields. "
            "At least one of headers or params must be provided."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name of the API."},
                "headers": {
                    "description": "Headers to add. Array of {field_name, field_value} objects.",
                },
                "params": {
                    "description": "Query params to add. Array of {field_name, field_value} objects.",
                },
            },
            "required": ["name"],
        },
    )


def search_apis_tool() -> Tool:
    return Tool(
        name="search_apis",
        description=(
            "Search for APIs across all collections by name substring, URL substring, or HTTP method. "
            "At least one of name, url, or method must be provided."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Case-insensitive substring to match against API name."},
                "url": {"type": "string", "description": "Case-insensitive substring to match against API URL."},
                "method": {"type": "string", "description": "HTTP method to filter by (exact match, e.g. GET, POST)."},
            },
            "required": [],
        },
    )


def list_apis_in_collection_tool() -> Tool:
    return Tool(
        name="list_apis_in_collection",
        description=(
            "List all APIs within a named collection. "
            "By default traverses the full subtree (recursive=true). "
            "Set recursive=false to return only direct child APIs."
        ),
        inputSchema={
            "type": "object",
            "properties": {
                "collection_name": {"type": "string", "description": "Name of the collection to list APIs from."},
                "recursive": {
                    "type": "boolean",
                    "description": "Whether to include APIs from all nested sub-collections. Default: true.",
                    "default": True,
                },
            },
            "required": ["collection_name"],
        },
    )


def update_api_param_header_tool() -> Tool:
    return Tool(
        name="update_api_param_header",
        description="Update the value of an existing header or query param on an API.",
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name of the API."},
                "field_name": {"type": "string", "description": "Name of the header or param to update."},
                "field_value": {"type": "string", "description": "New value for the field."},
                "field_type": {
                    "type": "string",
                    "enum": ["header", "param"],
                    "description": "Whether the field is a header or a query param.",
                },
            },
            "required": ["name", "field_name", "field_value", "field_type"],
        },
    )


def delete_api_param_header_tool() -> Tool:
    return Tool(
        name="delete_api_param_header",
        description="Delete a header or query param from an API by field name.",
        inputSchema={
            "type": "object",
            "properties": {
                "name": {"type": "string", "description": "Name of the API."},
                "field_name": {"type": "string", "description": "Name of the header or param to delete."},
            },
            "required": ["name", "field_name"],
        },
    )


# ---------------------------------------------------------------------------
# Handlers (new)
# ---------------------------------------------------------------------------

async def handle_add_api_param_headers(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    if not name:
        return [TextContent(type="text", text="Error: name is required.")]

    headers = arguments.get("headers") or []
    params = arguments.get("params") or []

    if isinstance(headers, str):
        try:
            headers = json.loads(headers)
        except json.JSONDecodeError:
            return [TextContent(type="text", text="Error: headers must be a valid JSON array of {field_name, field_value} objects.")]
    if isinstance(params, str):
        try:
            params = json.loads(params)
        except json.JSONDecodeError:
            return [TextContent(type="text", text="Error: params must be a valid JSON array of {field_name, field_value} objects.")]

    if not headers and not params:
        return [TextContent(type="text", text="Error: at least one of headers or params must be provided.")]

    try:
        async with MaerisClient() as client:
            matched, api_id, warning = await _find_api_by_name(client, name)
            if not matched:
                return [TextContent(type="text", text=warning)]

            # Fetch existing headers/params and merge to avoid backend overwrite behaviour
            detail = await client.get_api(api_id)
            param_headers = detail.get("param_headers", [])
            existing_headers = [
                {"field_name": ph["field_name"], "field_value": ph.get("field_value", "")}
                for ph in param_headers if (ph.get("type") or "").upper() == "HEADERS"
            ]
            existing_params = [
                {"field_name": ph["field_name"], "field_value": ph.get("field_value", "")}
                for ph in param_headers if (ph.get("type") or "").upper() == "PARAMS"
            ]

            new_header_names = {h["field_name"].lower() for h in headers}
            new_param_names = {p["field_name"].lower() for p in params}

            merged_headers = [h for h in existing_headers if h["field_name"].lower() not in new_header_names] + headers
            merged_params = [p for p in existing_params if p["field_name"].lower() not in new_param_names] + params

            await client.add_param_header(api_id, merged_headers, merged_params)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    result = {
        "status": "updated",
        "id": api_id,
        "name": name,
        "headersAdded": len(headers),
        "paramsAdded": len(params),
        "totalHeaders": len(merged_headers),
        "totalParams": len(merged_params),
        "message": f"Added {len(headers)} header(s) and {len(params)} param(s) to API '{name}'. Total: {len(merged_headers)} header(s), {len(merged_params)} param(s).",
    }
    if warning:
        result["warning"] = warning
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_search_apis(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name_filter = arguments.get("name")
    url_filter = arguments.get("url")
    method_filter = arguments.get("method")

    if not any([name_filter, url_filter, method_filter]):
        return [TextContent(type="text", text="Error: at least one of name, url, or method is required to search.")]

    try:
        async with MaerisClient() as client:
            all_apis = await client.get_all_apis()
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    filtered = []
    for api in all_apis:
        if name_filter and name_filter.lower() not in (api.get("name") or "").lower():
            continue
        if url_filter and url_filter.lower() not in (api.get("url") or "").lower():
            continue
        if method_filter:
            api_method = (api.get("type") or api.get("method") or "").upper()
            if api_method != method_filter.upper():
                continue
        filtered.append({
            "id": api.get("api_id") or api.get("id"),
            "name": api.get("name"),
            "method": (api.get("type") or api.get("method", "")).upper(),
            "url": api.get("url"),
            "collectionId": api.get("collection_id") or api.get("collectionId"),
            "collectionName": api.get("collection_name") or api.get("collectionName"),
        })

    filters_used = []
    if name_filter:
        filters_used.append(f"name='{name_filter}'")
    if url_filter:
        filters_used.append(f"url='{url_filter}'")
    if method_filter:
        filters_used.append(f"method='{method_filter}'")

    result = {
        "count": len(filtered),
        "filters": ", ".join(filters_used),
        "apis": filtered,
        "message": f"Found {len(filtered)} API(s) matching {', '.join(filters_used)}." if filtered else f"No APIs found matching {', '.join(filters_used)}. Try broadening your search or use list_apis to see all available APIs.",
    }
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_list_apis_in_collection(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    collection_name = arguments.get("collection_name")
    if not collection_name:
        return [TextContent(type="text", text="Error: collection_name is required.")]

    recursive = arguments.get("recursive", True)

    try:
        async with MaerisClient() as client:
            collection = await _find_collection_by_name(client, collection_name)
            if not collection:
                return [TextContent(type="text", text=f"Error: no collection found with name '{collection_name}'. Use list_collections to see available collections.")]
            collection_id = collection["api_collection_id"]

            if not recursive:
                children = await _get_children(client, collection_id)
                apis = [
                    {
                        "id": api["api_id"],
                        "name": api["name"],
                        "method": api["method"].upper(),
                        "url": api["url"],
                        "collectionId": collection_id,
                        "collectionName": collection_name,
                    }
                    for api in children.get("apis", [])
                ]
            else:
                apis = await _collect_all_apis_in_subtree(client, collection_id, collection_name)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    mode = "recursive" if recursive else "direct children only"
    result = {
        "count": len(apis),
        "collection": collection_name,
        "mode": mode,
        "apis": apis,
        "message": f"Found {len(apis)} API(s) in collection '{collection_name}' ({mode})." if apis else f"No APIs found in collection '{collection_name}' ({mode}). Use add_api or add_apis_bulk to add APIs.",
    }
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_update_api_param_header(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    field_name = arguments.get("field_name")
    field_value = arguments.get("field_value")
    field_type = arguments.get("field_type")

    if not all([name, field_name, field_value is not None, field_type]):
        return [TextContent(type="text", text="Error: name, field_name, field_value, and field_type are all required.")]

    if field_type not in ("header", "param"):
        return [TextContent(type="text", text="Error: field_type must be 'header' or 'param'.")]

    try:
        async with MaerisClient() as client:
            matched, api_id, warning = await _find_api_by_name(client, name)
            if not matched:
                return [TextContent(type="text", text=warning)]

            await client.update_param_header(api_id, field_name, field_value, field_type)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    result = {
        "status": "updated",
        "id": api_id,
        "fieldName": field_name,
        "fieldValue": field_value,
        "fieldType": field_type,
        "message": f"{field_type.capitalize()} '{field_name}' updated on API '{name}'.",
    }
    if warning:
        result["warning"] = warning
    return [TextContent(type="text", text=json.dumps(result, indent=2))]


async def handle_delete_api_param_header(arguments: dict) -> list[TextContent]:
    auth_err = _check_auth()
    if auth_err:
        return [TextContent(type="text", text=auth_err)]

    name = arguments.get("name")
    field_name = arguments.get("field_name")

    if not all([name, field_name]):
        return [TextContent(type="text", text="Error: both name and field_name are required.")]

    try:
        async with MaerisClient() as client:
            matched, api_id, warning = await _find_api_by_name(client, name)
            if not matched:
                return [TextContent(type="text", text=warning)]

            await client.delete_param_header(api_id, field_name)
    except MaerisClientError as exc:
        return [TextContent(type="text", text=_format_client_error(exc))]

    result = {
        "status": "deleted",
        "id": api_id,
        "fieldName": field_name,
        "message": f"Field '{field_name}' removed from API '{name}'.",
    }
    if warning:
        result["warning"] = warning
    return [TextContent(type="text", text=json.dumps(result, indent=2))]

