"""
App config management tools for FiftyOne MCP server.

| Copyright 2017-2026, Voxel51, Inc.
| `voxel51.com <https://voxel51.com/>`_
|
"""

import json
import logging

import fiftyone as fo
from mcp.types import Tool, TextContent

from .utils import format_response


logger = logging.getLogger(__name__)


def _serialize_color_scheme(cs):
    """Serializes a ColorScheme to a JSON-compatible dict.

    Args:
        cs: a :class:`fiftyone.core.odm.dataset.ColorScheme`, or None

    Returns:
        a dict, or None
    """
    if cs is None:
        return None

    return {
        "color_by": cs.color_by,
        "color_pool": list(cs.color_pool) if cs.color_pool else None,
        "fields": list(cs.fields) if cs.fields else None,
        "opacity": cs.opacity,
        "multicolor_keypoints": cs.multicolor_keypoints,
        "show_skeletons": cs.show_skeletons,
    }


def _serialize_active_fields(af):
    """Serializes an ActiveFields instance to a JSON-compatible dict.

    Args:
        af: a :class:`fiftyone.core.odm.dataset.ActiveFields`, or None

    Returns:
        a dict, or None
    """
    if af is None:
        return None

    return {
        "paths": list(af.paths) if af.paths else [],
        "exclude": af.exclude,
    }


def _serialize_sidebar_group(sg):
    """Serializes a SidebarGroupDocument to a JSON-compatible dict.

    Args:
        sg: a :class:`fiftyone.core.odm.dataset.SidebarGroupDocument`

    Returns:
        a dict
    """
    return {
        "name": sg.name,
        "paths": list(sg.paths) if sg.paths else [],
        "expanded": sg.expanded,
    }


def _serialize_app_config(ac):
    """Serializes a DatasetAppConfig to a JSON-compatible dict.

    Args:
        ac: a :class:`fiftyone.core.odm.dataset.DatasetAppConfig`, or None

    Returns:
        a dict
    """
    if ac is None:
        return {}

    return {
        "grid_media_field": ac.grid_media_field,
        "modal_media_field": ac.modal_media_field,
        "media_fields": list(ac.media_fields) if ac.media_fields else None,
        "color_scheme": _serialize_color_scheme(ac.color_scheme),
        "sidebar_groups": (
            [_serialize_sidebar_group(sg) for sg in ac.sidebar_groups]
            if ac.sidebar_groups
            else None
        ),
        "active_fields": _serialize_active_fields(ac.active_fields),
    }


def get_app_config(dataset_name):
    """Returns the full app config for a dataset.

    Args:
        dataset_name: the name of the dataset

    Returns:
        a dict with the full app config
    """
    try:
        dataset = fo.load_dataset(dataset_name)
        return format_response(
            {
                "dataset_name": dataset_name,
                "app_config": _serialize_app_config(dataset.app_config),
            }
        )
    except Exception as e:
        logger.error("Failed to get app config for '%s': %s", dataset_name, e)
        return format_response(None, success=False, error=str(e))


def get_color_scheme(dataset_name):
    """Returns the color scheme config for a dataset.

    Args:
        dataset_name: the name of the dataset

    Returns:
        a dict with the color scheme
    """
    try:
        dataset = fo.load_dataset(dataset_name)
        return format_response(
            {
                "dataset_name": dataset_name,
                "color_scheme": _serialize_color_scheme(
                    dataset.app_config.color_scheme
                ),
            }
        )
    except Exception as e:
        logger.error(
            "Failed to get color scheme for '%s': %s", dataset_name, e
        )
        return format_response(None, success=False, error=str(e))


def set_color_scheme(
    dataset_name,
    color_by=None,
    color_pool=None,
    fields=None,
):
    """Sets the color scheme for a dataset.

    Writes the color scheme to the database via ``dataset.save()``.

    Args:
        dataset_name: the name of the dataset
        color_by (None): how to color annotations. One of ``"field"``,
            ``"value"``, ``"instance"``, or None to use the default
        color_pool (None): a list of hex color strings to use as the
            color pool (e.g., ``["#FF0000", "#00FF00"]``)
        fields (None): a list of per-field color config dicts. Each dict
            may contain ``path``, ``fieldColor``, ``colorByAttribute``,
            and ``valueColors``

    Returns:
        a dict with the updated color scheme
    """
    try:
        dataset = fo.load_dataset(dataset_name)
        dataset.app_config.color_scheme = fo.ColorScheme(
            color_by=color_by,
            color_pool=color_pool,
            fields=fields,
        )
        dataset.save()
        return format_response(
            {
                "dataset_name": dataset_name,
                "color_scheme": _serialize_color_scheme(
                    dataset.app_config.color_scheme
                ),
            }
        )
    except Exception as e:
        logger.error(
            "Failed to set color scheme for '%s': %s", dataset_name, e
        )
        return format_response(None, success=False, error=str(e))


def get_sidebar_groups(dataset_name):
    """Returns the sidebar group configuration for a dataset.

    Args:
        dataset_name: the name of the dataset

    Returns:
        a dict with the sidebar groups list
    """
    try:
        dataset = fo.load_dataset(dataset_name)
        groups = dataset.app_config.sidebar_groups
        return format_response(
            {
                "dataset_name": dataset_name,
                "sidebar_groups": (
                    [_serialize_sidebar_group(g) for g in groups]
                    if groups
                    else None
                ),
            }
        )
    except Exception as e:
        logger.error(
            "Failed to get sidebar groups for '%s': %s", dataset_name, e
        )
        return format_response(None, success=False, error=str(e))


def set_sidebar_groups(dataset_name, groups):
    """Replaces the sidebar group configuration for a dataset.

    Writes the new groups to the database via ``dataset.save()``.

    Args:
        dataset_name: the name of the dataset
        groups: a list of dicts with keys ``name`` (str, required),
            ``paths`` (list of str, optional), and ``expanded``
            (bool, optional)

    Returns:
        a dict with the updated sidebar groups
    """
    try:
        dataset = fo.load_dataset(dataset_name)
        dataset.app_config.sidebar_groups = [
            fo.SidebarGroupDocument(
                name=g["name"],
                paths=g.get("paths", []),
                expanded=g.get("expanded"),
            )
            for g in groups
        ]
        dataset.save()
        return format_response(
            {
                "dataset_name": dataset_name,
                "sidebar_groups": [
                    _serialize_sidebar_group(sg)
                    for sg in dataset.app_config.sidebar_groups
                ],
            }
        )
    except Exception as e:
        logger.error(
            "Failed to set sidebar groups for '%s': %s", dataset_name, e
        )
        return format_response(None, success=False, error=str(e))


def set_active_fields(dataset_name, paths, exclude=False):
    """Controls which fields are visible in the App sidebar.

    Writes the active fields config to the database via ``dataset.save()``.

    Args:
        dataset_name: the name of the dataset
        paths: a list of field path strings
        exclude (False): if True, hide the listed paths; if False, show
            only the listed paths

    Returns:
        a dict with the updated active fields config
    """
    try:
        dataset = fo.load_dataset(dataset_name)
        dataset.app_config.active_fields = fo.ActiveFields(
            paths=paths,
            exclude=exclude,
        )
        dataset.save()
        return format_response(
            {
                "dataset_name": dataset_name,
                "active_fields": _serialize_active_fields(
                    dataset.app_config.active_fields
                ),
            }
        )
    except Exception as e:
        logger.error(
            "Failed to set active fields for '%s': %s", dataset_name, e
        )
        return format_response(None, success=False, error=str(e))


def get_app_config_tools():
    """Gets the list of app config MCP tools.

    Returns:
        a list of :class:`mcp.types.Tool` instances
    """
    return [
        Tool(
            name="get_app_config",
            description=(
                "Get the full app config for a dataset including color "
                "scheme, sidebar groups, active fields, and media field "
                "settings."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "dataset_name": {
                        "type": "string",
                        "description": "Name of the dataset",
                    },
                },
                "required": ["dataset_name"],
            },
        ),
        Tool(
            name="get_color_scheme",
            description=(
                "Get the color scheme configuration for a dataset. "
                "Returns color_by mode, color pool, and per-field "
                "color settings."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "dataset_name": {
                        "type": "string",
                        "description": "Name of the dataset",
                    },
                },
                "required": ["dataset_name"],
            },
        ),
        Tool(
            name="set_color_scheme",
            description=(
                "Set the color scheme for a dataset. Persists to the "
                "database. Supports setting color_by mode, a shared "
                "color pool, and per-field color overrides."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "dataset_name": {
                        "type": "string",
                        "description": "Name of the dataset",
                    },
                    "color_by": {
                        "type": "string",
                        "enum": ["field", "value", "instance"],
                        "description": (
                            "How to color annotations. One of 'field', "
                            "'value', or 'instance'"
                        ),
                    },
                    "color_pool": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": (
                            "List of hex color strings to use as the "
                            "color pool (e.g., ['#FF0000', '#00FF00'])"
                        ),
                    },
                    "fields": {
                        "type": "array",
                        "items": {"type": "object"},
                        "description": (
                            "Per-field color config dicts. Each dict may "
                            "contain: path, fieldColor, colorByAttribute, "
                            "valueColors (list of {value, color} dicts)"
                        ),
                    },
                },
                "required": ["dataset_name"],
            },
        ),
        Tool(
            name="get_sidebar_groups",
            description=(
                "Get the sidebar group configuration for a dataset. "
                "Returns the list of groups with their field paths and "
                "expanded state."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "dataset_name": {
                        "type": "string",
                        "description": "Name of the dataset",
                    },
                },
                "required": ["dataset_name"],
            },
        ),
        Tool(
            name="set_sidebar_groups",
            description=(
                "Replace the sidebar group configuration for a dataset. "
                "Persists to the database. Each group has a name, a list "
                "of field paths, and an optional expanded flag."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "dataset_name": {
                        "type": "string",
                        "description": "Name of the dataset",
                    },
                    "groups": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "name": {
                                    "type": "string",
                                    "description": "Group name",
                                },
                                "paths": {
                                    "type": "array",
                                    "items": {"type": "string"},
                                    "description": "Field paths in this group",
                                },
                                "expanded": {
                                    "type": "boolean",
                                    "description": (
                                        "Whether this group is expanded "
                                        "by default"
                                    ),
                                },
                            },
                            "required": ["name"],
                        },
                        "description": "List of sidebar group definitions",
                    },
                },
                "required": ["dataset_name", "groups"],
            },
        ),
        Tool(
            name="set_active_fields",
            description=(
                "Control which fields are visible in the App sidebar. "
                "Persists to the database. Use exclude=false (default) "
                "to show only the listed paths, or exclude=true to hide "
                "them."
            ),
            inputSchema={
                "type": "object",
                "properties": {
                    "dataset_name": {
                        "type": "string",
                        "description": "Name of the dataset",
                    },
                    "paths": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "List of field path strings",
                    },
                    "exclude": {
                        "type": "boolean",
                        "description": (
                            "If true, hide the listed paths. "
                            "If false (default), show only the listed paths"
                        ),
                        "default": False,
                    },
                },
                "required": ["dataset_name", "paths"],
            },
        ),
    ]


_TOOL_NAMES = {
    "get_app_config",
    "get_color_scheme",
    "set_color_scheme",
    "get_sidebar_groups",
    "set_sidebar_groups",
    "set_active_fields",
}

_REQUIRED_ARGS = {
    "get_app_config": ["dataset_name"],
    "get_color_scheme": ["dataset_name"],
    "set_color_scheme": ["dataset_name"],
    "get_sidebar_groups": ["dataset_name"],
    "set_sidebar_groups": ["dataset_name", "groups"],
    "set_active_fields": ["dataset_name", "paths"],
}

_TOOL_HANDLERS = {
    "get_app_config": lambda a: get_app_config(a["dataset_name"]),
    "get_color_scheme": lambda a: get_color_scheme(a["dataset_name"]),
    "set_color_scheme": lambda a: set_color_scheme(
        a["dataset_name"],
        color_by=a.get("color_by"),
        color_pool=a.get("color_pool"),
        fields=a.get("fields"),
    ),
    "get_sidebar_groups": lambda a: get_sidebar_groups(a["dataset_name"]),
    "set_sidebar_groups": lambda a: set_sidebar_groups(
        a["dataset_name"],
        a["groups"],
    ),
    "set_active_fields": lambda a: set_active_fields(
        a["dataset_name"],
        a["paths"],
        exclude=a.get("exclude", False),
    ),
}


async def handle_tool_call(name, arguments):
    """Handles app config tool calls.

    Args:
        name: the name of the tool
        arguments: a dict of arguments for the tool

    Returns:
        a list of :class:`mcp.types.TextContent` instances
    """
    try:
        if name not in _TOOL_NAMES:
            result = format_response(
                None, success=False, error=f"Unknown tool: {name}"
            )
        else:
            missing = [
                arg for arg in _REQUIRED_ARGS[name] if arg not in arguments
            ]
            if missing:
                result = format_response(
                    None,
                    success=False,
                    error=(
                        f"{missing[0]} is required"
                        if len(missing) == 1
                        else f"Required arguments missing: "
                        f"{', '.join(missing)}"
                    ),
                )
            else:
                result = _TOOL_HANDLERS[name](arguments)

        return [TextContent(type="text", text=json.dumps(result, indent=2))]

    except Exception as e:
        logger.error("Error handling app config tool '%s': %s", name, e)
        error_result = format_response(None, success=False, error=str(e))
        return [
            TextContent(type="text", text=json.dumps(error_result, indent=2))
        ]
