import logging

from cognite.client import CogniteClient
from cognite.client.data_classes.data_modeling import ViewId, aggregations
from cognite.client.data_classes.data_modeling.instances import InstanceSort
from mcp.server.fastmcp import FastMCP

from cog_mcp.config import Config
from cog_mcp.errors import error_response
from cog_mcp.formatting import format_aggregation, format_instances
from cog_mcp.parsing import parse_edge_id, parse_json_param, parse_node_id

logger = logging.getLogger(__name__)


def register_instance_tools(mcp: FastMCP, client: CogniteClient, config: Config) -> None:
    """Register list / search / aggregate / retrieve tools."""

    @mcp.tool()
    def list_instances(
        view_space: str,
        view_external_id: str,
        view_version: str,
        filter: str | dict | None = None,
        limit: int = 100,
        sort: str | list | dict | None = None,
        instance_type: str = "node",
    ) -> str:
        """List instances of a view with optional filtering.

        Use the list_views tool to find valid view coordinates. For filter/sort property
        names, call get_view_schema.

        Instance space filters are automatically applied from configuration.
        The SDK handles pagination internally — use `limit` to control how many
        instances are returned. Use aggregate_instances first to check total counts.

        For filter syntax, call the get_filter_docs tool.

        Args:
            view_space: Space of the view to list instances from.
            view_external_id: External ID of the view.
            view_version: Version of the view.
            filter: Optional JSON filter expression (e.g. {"equals": {"property": ["viewSpace", "viewId/version", "prop"], "value": "x"}}).
            limit: Max instances to return. Default 100. Use -1 for ALL instances (auto-paginates).
            sort: Optional JSON sort spec (e.g. [{"property": ["space", "viewId/version", "prop"], "direction": "ascending"}]).
            instance_type: "node" or "edge". Default "node".
        """
        view_id = ViewId(space=view_space, external_id=view_external_id, version=view_version)

        parsed_filter = parse_json_param(filter)

        parsed_sort = None
        if sort:
            raw_sort = parse_json_param(sort)
            if isinstance(raw_sort, list):
                parsed_sort = [
                    InstanceSort(
                        property=s["property"],
                        direction=s.get("direction", "ascending"),
                        nulls_first=s.get("nullsFirst"),
                    )
                    for s in raw_sort
                ]
            else:
                parsed_sort = InstanceSort(
                    property=raw_sort["property"],
                    direction=raw_sort.get("direction", "ascending"),
                )

        effective_limit = None if limit == -1 else limit

        result = client.data_modeling.instances.list(
            instance_type=instance_type,
            sources=[view_id],
            space=config.instance_spaces,
            filter=parsed_filter,
            limit=effective_limit,
            sort=parsed_sort,
        )

        total = None
        if effective_limit is not None and len(result) >= effective_limit:
            try:
                agg = client.data_modeling.instances.aggregate(
                    view=view_id,
                    aggregates=aggregations.Count("externalId"),
                    space=config.instance_spaces,
                    filter=parsed_filter,
                )
                if hasattr(agg, "value"):
                    total = int(agg.value)
                elif isinstance(agg, list) and agg:
                    total = int(agg[0].value)
            except Exception as e:
                logger.debug("Failed to fetch total count for list_instances: %s", e)

        return format_instances(list(result), total=total)

    @mcp.tool()
    def search_instances(
        view_space: str,
        view_external_id: str,
        view_version: str,
        query: str,
        properties: str | list | None = None,
        filter: str | dict | None = None,
        limit: int = 100,
        instance_type: str = "node",
        operator: str | None = None,
    ) -> str:
        """Full-text search across instances of a view.

        Use the list_views tool to find valid view coordinates. For property names to
        search or filter on, call get_view_schema.

        Instance space filters are automatically applied from configuration.
        Note: Search is eventually consistent — new/updated data may take a few seconds to appear.

        For filter syntax, call the get_filter_docs tool.

        Args:
            view_space: Space of the view to search in.
            view_external_id: External ID of the view.
            view_version: Version of the view.
            query: Search text. Tokens are matched case-insensitively. Last token uses prefix matching.
            properties: Optional JSON list of property names to search (e.g. ["name", "description"]). If not specified, all text fields are searched.
            filter: Optional JSON filter to narrow results (e.g. {"range": {"property": [...], "gt": 2015}}).
            limit: Max results. Default 100. Use -1 for max (1000).
            instance_type: "node" or "edge". Default "node".
            operator: "AND" (all terms must match) or "OR" (any term matches). Default: API default.
        """
        view_id = ViewId(space=view_space, external_id=view_external_id, version=view_version)

        parsed_filter = parse_json_param(filter)
        parsed_properties = parse_json_param(properties)
        effective_limit = None if limit == -1 else limit

        result = client.data_modeling.instances.search(
            view=view_id,
            query=query,
            instance_type=instance_type,
            properties=parsed_properties,
            space=config.instance_spaces,
            filter=parsed_filter,
            limit=effective_limit,
            operator=operator,
        )

        return format_instances(list(result))

    @mcp.tool()
    def aggregate_instances(
        view_space: str,
        view_external_id: str,
        view_version: str,
        aggregate: str,
        property: str | None = None,
        group_by: str | None = None,
        filter: str | dict | None = None,
        query: str | None = None,
        instance_type: str = "node",
        histogram_interval: float | None = None,
    ) -> str:
        """Aggregate instances of a view (count, sum, avg, min, max, histogram).

        Use the list_views tool to find valid view coordinates. Call get_view_schema for
        valid property and group_by names.

        Instance space filters are automatically applied. Use this to understand data volumes
        before listing — especially important for large datasets.

        Tip: To get the total number of instances, use aggregate="count" with property="externalId".

        For filter syntax, call the get_filter_docs tool.

        Args:
            view_space: Space of the view.
            view_external_id: External ID of the view.
            view_version: Version of the view.
            aggregate: One of "count", "sum", "avg", "min", "max", "histogram".
            property: Property to aggregate. Required for all aggregates. Use "externalId" for total count.
            group_by: Optional property name to group by (e.g. "status"). For multiple, use comma-separated (e.g. "status,type").
            filter: Optional JSON filter expression.
            query: Optional search query to filter before aggregating.
            instance_type: "node" or "edge". Default "node".
            histogram_interval: Bucket interval for histogram aggregation. Required if aggregate is "histogram".
        """
        view_id = ViewId(space=view_space, external_id=view_external_id, version=view_version)

        agg_map = {
            "count": aggregations.Count,
            "sum": aggregations.Sum,
            "avg": aggregations.Avg,
            "min": aggregations.Min,
            "max": aggregations.Max,
        }

        if aggregate == "histogram":
            if not property or histogram_interval is None:
                return error_response("histogram requires 'property' and 'histogram_interval'")
            agg = aggregations.Histogram(property=property, interval=histogram_interval)
        elif aggregate in agg_map:
            if not property:
                return error_response(f"'{aggregate}' requires 'property'")
            agg = agg_map[aggregate](property=property)
        else:
            return error_response(
                f"Unknown aggregate: {aggregate}. Use one of: count, sum, avg, min, max, histogram"
            )

        parsed_filter = parse_json_param(filter)

        parsed_group_by: str | list[str] | None = None
        if group_by:
            parts = [p.strip() for p in group_by.split(",")]
            parsed_group_by = parts if len(parts) > 1 else parts[0]

        result = client.data_modeling.instances.aggregate(
            view=view_id,
            aggregates=agg,
            group_by=parsed_group_by,
            instance_type=instance_type,
            query=query,
            space=config.instance_spaces,
            filter=parsed_filter,
        )

        if group_by and not result:
            try:
                check = client.data_modeling.instances.aggregate(
                    view=view_id,
                    aggregates=aggregations.Count(property=property or "externalId"),
                    instance_type=instance_type,
                    space=config.instance_spaces,
                    filter=parsed_filter,
                )
                total = check.value if hasattr(check, "value") else (check[0].value if check else 0)
                hint = (
                    f"group_by '{group_by}' returned no results. "
                    f"A count on '{property or 'externalId'}' (without group_by) shows {total} values. "
                )
                if total == 0:
                    hint += "This is because no instances match the provided filter."
                else:
                    hint += (
                        f"This suggests that while {total} instances match your filter, "
                        f"none of them have a populated value for the '{group_by}' property."
                    )
                return format_aggregation(result, hint=hint)
            except Exception as e:
                logger.warning("Failed to get diagnostic count for empty group_by: %s", e)

        return format_aggregation(result)

    @mcp.tool()
    def retrieve_instances(
        nodes: str | list | None = None,
        edges: str | list | None = None,
        view_space: str | None = None,
        view_external_id: str | None = None,
        view_version: str | None = None,
    ) -> str:
        """Retrieve specific instances by their IDs.

        Use the list_views tool to find valid view coordinates if scoping by view.

        Args:
            nodes: JSON list of node IDs (e.g. [{"space": "mySpace", "externalId": "node1"}]).
            edges: JSON list of edge IDs (e.g. [{"space": "mySpace", "externalId": "edge1"}]).
            view_space: Optional view space to scope returned properties.
            view_external_id: Optional view external ID.
            view_version: Optional view version.
        """
        parsed_nodes = None
        if nodes:
            raw_nodes = parse_json_param(nodes)
            parsed_nodes = [parse_node_id(n, f"nodes[{i}]") for i, n in enumerate(raw_nodes)]

        parsed_edges = None
        if edges:
            raw_edges = parse_json_param(edges)
            parsed_edges = [parse_edge_id(e, f"edges[{i}]") for i, e in enumerate(raw_edges)]

        sources = None
        if view_space and view_external_id and view_version:
            sources = [ViewId(space=view_space, external_id=view_external_id, version=view_version)]

        result = client.data_modeling.instances.retrieve(
            nodes=parsed_nodes,
            edges=parsed_edges,
            sources=sources,
        )

        all_items = []
        if result.nodes:
            all_items.extend(result.nodes)
        if result.edges:
            all_items.extend(result.edges)

        return format_instances(all_items)
