"""Discovery tools: search and browse the open data catalogue.

These five tools cover the pre-download phase — finding what exists before
committing to a download. They fan out to all portals in parallel so a single
call covers Ontario, Toronto, Ottawa, and the Waterloo-region portals at once.

Why separate tools instead of one "browse" tool with a mode parameter?

- ``search_datasets`` — full-text search with filters; the primary entry point.
- ``list_organizations`` — ministry/org enumeration needed when the user wants to
  explore by publisher rather than topic (e.g. "what does the Ministry of Health
  publish?"). Search can't answer this efficiently.
- ``list_topics`` — tag enumeration for open-ended topic browsing. A user asking
  "what environmental data exists?" benefits from seeing all 80 environment-tagged
  topics rather than a search result capped at 5-50.
- ``find_related_datasets`` — serendipitous discovery via shared tags and org.
  Useful after finding one dataset; search would require the model to re-derive
  the query from scratch.
- ``list_portals`` — lightweight registry lookup; no network fan-out, pure config.
  Kept separate so the model can inspect available portals without any API call.

Consolidation considered and rejected: merging these into a single tool with a
``mode`` parameter would make the docstring unreadably long and force the model to
choose the right mode before knowing which is appropriate. Five short, focused
docstrings are more reliable for tool selection than one long one with branches.
"""
from __future__ import annotations

import logging

from fastmcp import Context

from ontario_data.formatting import format_search_results, md_response
from ontario_data.server import READONLY, mcp
from ontario_data.utils import (
    get_lifespan_state,
    fan_out,
    get_deps,
    resolve_dataset,
)

logger = logging.getLogger("ontario_data.discovery")


@mcp.tool(annotations=READONLY)
async def search_datasets(
    query: str,
    organization: str | None = None,
    resource_format: str | None = None,
    update_frequency: str | None = None,
    sort_by: str = "relevance asc, metadata_modified desc",
    limit: int = 5,
    portal: str | None = None,
    ctx: Context = None,
) -> str:
    """Search for datasets across all open data portals (Ontario, Toronto, Ottawa).

    Results include portal-prefixed IDs (e.g. toronto:abc123). Use these IDs with
    get_dataset_info to view full metadata, then download_resource to cache locally.
    Use get_resource_schema to check column types before writing queries.

    Searches fan out to all portals by default. Narrow to one portal with the portal param.

    Args:
        query: Search terms (e.g. "covid cases", "housing prices", "school enrollment")
        organization: Filter by ministry/org (e.g. "health", "education")
        resource_format: Filter by file format (e.g. "CSV", "JSON", "SHP")
        update_frequency: Filter by frequency (e.g. "yearly", "monthly", "daily")
        sort_by: Sort order (default: relevance)
        limit: Max results per portal (1-50)
        portal: Narrow to one portal (e.g. "ontario", "toronto"). Default: all portals.
    """
    configs = get_lifespan_state(ctx)["portal_configs"]
    filters = {}
    if organization:
        filters["organization"] = organization
    if resource_format:
        filters["res_format"] = resource_format
    if update_frequency:
        filters["update_frequency"] = update_frequency

    async def _search_one(portal_key: str) -> dict:
        ckan, _ = get_deps(ctx, portal_key)
        result = await ckan.package_search(
            query=query, filters=filters or None, sort=sort_by, rows=min(limit, 50),
        )
        datasets = []
        for ds in result["results"]:
            resources = ds.get("resources", [])
            formats = sorted(set(r.get("format", "").upper() for r in resources if r.get("format")))
            datasets.append({
                "id": f"{portal_key}:{ds['id']}",
                "name": ds.get("name"),
                "title": ds.get("title"),
                "organization": ds.get("organization", {}).get("title", "Unknown"),
                "description": (ds.get("notes") or "")[:200],
                "formats": formats,
                "num_resources": len(resources),
                "last_modified": ds.get("metadata_modified"),
                "update_frequency": ds.get("update_frequency", "unknown"),
            })
        return {
            "portal": portal_key,
            "portal_name": configs[portal_key].name,
            "total_count": result["count"],
            "returned": len(datasets),
            "datasets": datasets,
        }

    raw = await fan_out(ctx, portal, _search_one)

    results = []
    skipped = []
    for portal_key, result, error in raw:
        if error:
            skipped.append({"portal": portal_key, "portal_name": configs[portal_key].name, "reason": error})
        else:
            results.append(result)

    return format_search_results(
        query=query,
        portals_searched=len(results),
        results=results,
        skipped=skipped,
    )


@mcp.tool(annotations=READONLY)
async def list_organizations(
    include_counts: bool = True,
    portal: str | None = None,
    ctx: Context = None,
) -> str:
    """List government ministries and organizations with dataset counts across all portals.

    Args:
        include_counts: Include dataset counts per organization
        portal: Narrow to one portal. Default: all portals.
    """

    async def _list_orgs(portal_key: str) -> list[dict]:
        ckan, _ = get_deps(ctx, portal_key)
        orgs = await ckan.organization_list(all_fields=True, include_dataset_count=include_counts)
        result = []
        for org in orgs:
            result.append({
                "portal": portal_key,
                "name": org.get("name"),
                "title": org.get("title"),
                "dataset_count": org.get("package_count", 0),
                "description": (org.get("description") or "")[:150],
            })
        result.sort(key=lambda x: x["dataset_count"], reverse=True)
        return result

    raw = await fan_out(ctx, portal, _list_orgs)
    all_orgs = []
    for _, result, error in raw:
        if result and not error:
            all_orgs.extend(result)
    return md_response(organizations=all_orgs)


@mcp.tool(annotations=READONLY)
async def list_topics(
    query: str | None = None,
    portal: str | None = None,
    limit: int = 100,
    ctx: Context = None,
) -> str:
    """List top tags/topics used across data portals, ranked by dataset count.

    Args:
        query: Optional filter to match tag names
        portal: Narrow to one portal. Default: all portals.
        limit: Max tags to return (default 100). Use a higher value to see more.
    """

    async def _list_tags(portal_key: str) -> list[dict]:
        ckan, _ = get_deps(ctx, portal_key)
        tags = await ckan.tag_list(query=query, all_fields=True)
        if isinstance(tags, list) and tags and isinstance(tags[0], dict):
            return [{"portal": portal_key, "name": t["name"], "count": t.get("count", 0)} for t in tags]
        return [{"portal": portal_key, "name": t} for t in tags]

    raw = await fan_out(ctx, portal, _list_tags)
    all_tags = []
    for _, result, error in raw:
        if result and not error:
            all_tags.extend(result)

    # Sort by count descending so the most-used tags come first
    all_tags.sort(key=lambda t: t.get("count", 0), reverse=True)
    total = len(all_tags)
    all_tags = all_tags[:limit]

    note = f"Showing top {len(all_tags)} of {total} tags" if total > limit else f"{total} tags"
    return md_response(summary=note, topics=all_tags)


@mcp.tool(annotations=READONLY)
async def find_related_datasets(
    dataset_id: str,
    limit: int = 10,
    ctx: Context = None,
) -> str:
    """Find datasets related to a given dataset by shared tags and organization.

    Searches within the same portal as the source dataset only.

    Args:
        dataset_id: Prefixed dataset ID (e.g. "toronto:ttc-ridership") or bare ID
        limit: Max related datasets to return
    """
    portal, bare_id, source = await resolve_dataset(ctx, dataset_id)
    ckan, _ = get_deps(ctx, portal)
    tags = [t["name"] for t in source.get("tags", [])]
    org = source.get("organization", {}).get("name", "")

    related = []
    if tags:
        tag_query = " OR ".join(tags[:5])
        result = await ckan.package_search(query=tag_query, rows=min(limit + 5, 50))
        for ds in result["results"]:
            if ds["id"] != source["id"]:
                shared_tags = [t["name"] for t in ds.get("tags", []) if t["name"] in tags]
                related.append({
                    "id": f"{portal}:{ds['id']}",
                    "title": ds.get("title"),
                    "organization": ds.get("organization", {}).get("title", "Unknown"),
                    "shared_tags": shared_tags,
                    "relevance": "tags",
                })

    if org:
        result = await ckan.package_search(filters={"organization": org}, rows=min(limit, 50))
        seen_ids = {r["id"] for r in related}
        for ds in result["results"]:
            if ds["id"] != source["id"] and f"{portal}:{ds['id']}" not in seen_ids:
                related.append({
                    "id": f"{portal}:{ds['id']}",
                    "title": ds.get("title"),
                    "organization": ds.get("organization", {}).get("title", "Unknown"),
                    "shared_tags": [],
                    "relevance": "same_organization",
                })

    return md_response(
        source={"id": f"{portal}:{source['id']}", "title": source.get("title"), "tags": tags},
        related=related[:limit],
    )


@mcp.tool(annotations=READONLY)
async def list_portals(
    ctx: Context = None,
) -> str:
    """List all available data portals with their platform type and descriptions."""
    state = get_lifespan_state(ctx)
    configs = state["portal_configs"]

    portals = []
    for key, config in configs.items():
        portals.append({
            "key": key,
            "name": config.name,
            "base_url": config.base_url,
            "portal_type": str(config.portal_type),
            "description": config.description,
        })

    return md_response(portals=portals)
