"""FastMCP server for NASA GIBS — tools, resources, and prompts.

Wires the GIBS client, geocoding, capabilities parser, and colormap
interpreter into a cohesive MCP server that lets LLMs discover and
fetch satellite imagery via natural language.

IMPORTANT: Do NOT use `from __future__ import annotations` here.
FastMCP needs runtime types for Literal, list[str], etc.
"""

import asyncio
import atexit
import json
import logging
from datetime import date as Date

import httpx
from fastmcp import FastMCP
from fastmcp.server.context import Context
from fastmcp.server.middleware import Middleware
from fastmcp.utilities.types import Image

from mcgibs.capabilities import search_layers
from mcgibs.client import GIBSClient
from mcgibs.constants import PROJECTIONS
from mcgibs.geo import bbox_to_polar, expand_bbox
from mcgibs.models import BBox

log = logging.getLogger(__name__)

mcp = FastMCP(
    "mcgibs",
    instructions=(
        "NASA Global Imagery Browse Services (GIBS) — "
        "search 1000+ satellite visualization layers, fetch imagery by place name "
        "and date, and get natural-language explanations of what the colors mean."
    ),
)

# Shared client instance — initialized via on_initialize middleware
_client: GIBSClient | None = None


def _get_client() -> GIBSClient:
    """Retrieve the initialized GIBS client."""
    if _client is None:
        raise RuntimeError("GIBS client not initialized")
    return _client


def _shutdown_client() -> None:
    """Close the GIBS client on interpreter shutdown.

    FastMCP Middleware has no on_shutdown hook, so we use atexit to
    ensure the httpx connection pool is closed cleanly rather than
    relying on garbage collection.
    """
    global _client
    if _client is not None:
        client = _client
        _client = None
        try:
            loop = asyncio.new_event_loop()
            loop.run_until_complete(client.close())
            loop.close()
        except Exception:
            pass


atexit.register(_shutdown_client)


# --- Middleware: initialize client on session start ---


class GIBSInitMiddleware(Middleware):
    """Load GIBS capabilities when the first client connects."""

    async def on_initialize(self, context, call_next):
        global _client
        if _client is None:
            log.info("Initializing GIBS client and loading capabilities...")
            client = GIBSClient()
            try:
                await client.initialize()
            except Exception:
                # C2: clean up the HTTP client so we don't leak sockets,
                # then re-raise so the next connection can retry.
                await client.close()
                raise
            _client = client
            log.info("GIBS client ready with %d layers", len(_client.layer_index))
        return await call_next(context)


mcp.middleware.append(GIBSInitMiddleware())


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  TOOLS — Discovery
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


@mcp.tool(
    description="Search NASA GIBS satellite imagery layers by keyword. "
    "Returns matching layers with titles, identifiers, and date ranges."
)
async def search_gibs_layers(
    query: str,
    measurement: str | None = None,
    period: str | None = None,
    ongoing: bool | None = None,
    limit: int = 20,
) -> str:
    """Search across 1000+ GIBS visualization layers.

    Args:
        query: Keywords to search (e.g. "sea ice", "temperature", "MODIS fire").
        measurement: Filter by measurement category (e.g. "Sea Ice", "Temperature").
        period: Filter by temporal period ("Daily", "Monthly", "8-Day").
        ongoing: If True, only actively updated layers. If False, only ended datasets.
        limit: Maximum results to return.
    """
    client = _get_client()
    results = search_layers(
        client.layer_index,
        query,
        measurement,
        period,
        ongoing,
        limit,
    )

    if not results:
        return f"No layers found matching '{query}'."

    lines = [f"Found {len(results)} layer(s) matching '{query}':\n"]
    for layer in results:
        date_info = ""
        if layer.time:
            parts = []
            if layer.time.start:
                parts.append(f"from {layer.time.start}")
            if layer.time.end:
                parts.append(f"to {layer.time.end}")
            if layer.time.period:
                parts.append(f"({layer.time.period})")
            date_info = " — " + " ".join(parts) if parts else ""

        lines.append(f"  {layer.identifier}")
        lines.append(f"    {layer.title}{date_info}")
        if layer.has_colormap:
            lines.append("    [has colormap]")
        lines.append("")

    return "\n".join(lines)


@mcp.tool(
    description="Get detailed metadata for a specific GIBS layer including "
    "dates, resolution, formats, projections, and colormap availability."
)
async def get_layer_info(layer_id: str, ctx: Context) -> str:
    """Fetch full metadata for a layer.

    Args:
        layer_id: The GIBS layer identifier (e.g. "MODIS_Terra_CorrectedReflectance_TrueColor").
    """
    client = _get_client()
    layer = client.get_layer(layer_id)
    if layer is None:
        return f"Layer '{layer_id}' not found. Use search_gibs_layers to find valid identifiers."

    await ctx.report_progress(1, 2, "Fetching layer metadata...")
    await client.fetch_layer_metadata(layer_id)

    info = {
        "identifier": layer.identifier,
        "title": layer.title,
        "formats": layer.formats,
        "tile_matrix_sets": layer.tile_matrix_sets,
        "has_colormap": layer.has_colormap,
    }

    if layer.abstract:
        info["abstract"] = layer.abstract
    if layer.measurement:
        info["measurement"] = layer.measurement
    if layer.instrument:
        info["instrument"] = layer.instrument
    if layer.platform:
        info["platform"] = layer.platform
    if layer.period:
        info["period"] = layer.period
    if layer.ongoing is not None:
        info["ongoing"] = layer.ongoing
    if layer.day_night:
        info["day_night"] = layer.day_night
    if layer.description:
        info["description"] = layer.description
    if layer.time:
        info["time"] = {
            "start": layer.time.start,
            "end": layer.time.end,
            "period": layer.time.period,
            "default": layer.time.default,
        }
    if layer.bbox:
        info["bbox"] = {
            "west": layer.bbox.west,
            "south": layer.bbox.south,
            "east": layer.bbox.east,
            "north": layer.bbox.north,
        }

    await ctx.report_progress(2, 2, "Complete")
    return json.dumps(info, indent=2)


@mcp.tool(description="List all measurement categories available in GIBS with layer counts.")
async def list_measurements() -> str:
    """List measurement categories across all layers."""
    client = _get_client()

    # Collect measurements — we need to fetch metadata for layers
    # that haven't been enriched yet. For efficiency, work from
    # the layer titles which often encode the measurement.
    measurements: dict[str, int] = {}

    for layer in client.layer_index.values():
        key = layer.measurement or "Unknown"
        measurements[key] = measurements.get(key, 0) + 1

    if not measurements:
        return "No measurement categories available."

    lines = ["Measurement categories:\n"]
    for name, count in sorted(measurements.items()):
        lines.append(f"  {name}: {count} layer(s)")

    return "\n".join(lines)


@mcp.tool(description="Check available date ranges for a GIBS layer via WMTS DescribeDomains.")
async def check_layer_dates(layer_id: str, ctx: Context) -> str:
    """Query what dates are available for a specific layer.

    Args:
        layer_id: The GIBS layer identifier.
    """
    client = _get_client()
    layer = client.get_layer(layer_id)
    if layer is None:
        return f"Layer '{layer_id}' not found."

    # First, report what the capabilities document says
    lines = [f"Date information for {layer_id}:"]

    if layer.time:
        if layer.time.start:
            lines.append(f"  Start: {layer.time.start}")
        if layer.time.end:
            lines.append(f"  End: {layer.time.end}")
        if layer.time.period:
            lines.append(f"  Period: {layer.time.period}")
        if layer.time.default:
            lines.append(f"  Default/latest: {layer.time.default}")
    else:
        lines.append("  No time dimension (static layer)")

    # Try DescribeDomains for more precise info
    await ctx.report_progress(1, 2, "Querying live date range...")
    try:
        domains = await client.describe_domains(layer_id)
        if "time_domain" in domains:
            lines.append(f"  Live time domain: {domains['time_domain']}")
    except (httpx.HTTPError, RuntimeError) as exc:
        log.debug("DescribeDomains failed for %s: %s", layer_id, exc)

    await ctx.report_progress(2, 2, "Complete")
    return "\n".join(lines)


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  TOOLS — Imagery
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


async def _resolve_bbox(
    client: GIBSClient,
    bbox: list[float] | None,
    place: str | None,
) -> BBox:
    """Resolve a bounding box from either explicit coordinates or a place name."""
    if bbox and len(bbox) == 4:
        return BBox(west=bbox[0], south=bbox[1], east=bbox[2], north=bbox[3])

    if place:
        result = await client.resolve_place(place)
        if result:
            return expand_bbox(result.bbox, factor=0.1)
        raise ValueError(f"Could not geocode '{place}'. Try providing explicit bbox coordinates.")

    raise ValueError("Provide either 'bbox' [west, south, east, north] or 'place' name.")


def _pick_projection(bbox: BBox, projection: str) -> tuple[str, BBox]:
    """Resolve projection and transform bbox if needed.

    Returns (epsg, transformed_bbox). For polar projections the bbox is
    converted from geographic coordinates to projected meters.
    """
    if projection == "auto":
        if bbox.south >= 55:
            projection = "3413"
        elif bbox.north <= -55:
            projection = "3031"
        else:
            return ("4326", bbox)

    if projection in ("3413", "3031"):
        xmin, ymin, xmax, ymax = bbox_to_polar(bbox, projection)
        # model_construct bypasses geographic range validators — projected
        # coords are in meters, not degrees
        proj_bbox = BBox.model_construct(west=xmin, south=ymin, east=xmax, north=ymax)
        return (projection, proj_bbox)

    return (projection, bbox)


def _pick_format(layer) -> tuple[str, str]:
    """Choose the best image format for a layer.

    Colormapped layers need PNG to preserve exact pixel colors for
    reverse-mapping via query_point. True-color layers use JPEG.

    Returns (mime_type, short_name) e.g. ("image/png", "png").
    """
    if layer.has_colormap:
        return ("image/png", "png")
    if "image/jpeg" in layer.formats:
        return ("image/jpeg", "jpeg")
    return ("image/png", "png")


def _generate_dates(start: str, end: str, steps: int) -> list[str]:
    """Generate evenly-spaced dates between start and end (inclusive)."""
    d0 = Date.fromisoformat(start)
    d1 = Date.fromisoformat(end)
    if steps <= 1:
        return [start]
    if steps == 2:
        return [start, end]
    delta = (d1 - d0) / (steps - 1)
    return [(d0 + delta * i).isoformat() for i in range(steps)]


@mcp.tool(
    description="Fetch satellite imagery for a specific layer, date, and region. "
    "Provide either a place name or bbox coordinates. Returns the image."
)
async def get_imagery(
    layer_id: str,
    date: str,
    ctx: Context,
    bbox: list[float] | None = None,
    place: str | None = None,
    width: int = 1024,
    height: int = 1024,
    format: str = "auto",
    projection: str = "auto",
):
    """Fetch GIBS imagery via WMS.

    Args:
        layer_id: GIBS layer identifier.
        date: Date in YYYY-MM-DD format.
        bbox: Bounding box as [west, south, east, north] in decimal degrees.
        place: Place name to geocode (e.g. "California", "Amazon Basin").
        width: Image width in pixels.
        height: Image height in pixels.
        format: Image format — "jpeg", "png", or "auto" (picks PNG for colormapped layers).
        projection: EPSG code ("4326", "3857", "3413", "3031") or "auto" to detect from bbox.
    """
    client = _get_client()

    layer = client.get_layer(layer_id)
    if layer is None:
        return f"Layer '{layer_id}' not found."

    if format == "auto":
        mime_type, short_fmt = _pick_format(layer)
    else:
        mime_type, short_fmt = f"image/{format}", format

    await ctx.report_progress(1, 4, "Resolving location...")
    try:
        resolved_bbox = await _resolve_bbox(client, bbox, place)
    except Exception as exc:
        return str(exc)

    epsg, proj_bbox = _pick_projection(resolved_bbox, projection)

    await ctx.report_progress(2, 4, "Fetching imagery from GIBS...")
    image_bytes = await client.get_wms_image(
        layer_id, date, proj_bbox, width, height, mime_type, epsg,
    )

    description = (
        f"{layer.title} — {date}\n"
        f"Region: {place or resolved_bbox.wms_bbox}\n"
        f"Size: {width}x{height}"
    )
    if layer.has_colormap:
        description += "\nTip: use explain_layer_colormap to understand what the colors represent."

    await ctx.report_progress(4, 4, "Complete")
    return [description, Image(data=image_bytes, format=short_fmt)]


@mcp.tool(
    description="Compare a layer across two dates with a side-by-side image. "
    "Useful for tracking changes over time (fires, floods, ice extent)."
)
async def compare_dates(
    layer_id: str,
    date_before: str,
    date_after: str,
    ctx: Context,
    bbox: list[float] | None = None,
    place: str | None = None,
    projection: str = "auto",
):
    """Side-by-side comparison of two dates.

    Args:
        layer_id: GIBS layer identifier.
        date_before: Earlier date (YYYY-MM-DD).
        date_after: Later date (YYYY-MM-DD).
        bbox: Bounding box as [west, south, east, north].
        place: Place name to geocode.
        projection: EPSG code or "auto" to detect from bbox.
    """
    client = _get_client()

    layer = client.get_layer(layer_id)
    if layer is None:
        return f"Layer '{layer_id}' not found."

    await ctx.report_progress(1, 5, "Resolving location...")
    try:
        resolved_bbox = await _resolve_bbox(client, bbox, place)
    except Exception as exc:
        return str(exc)

    mime_type, short_fmt = _pick_format(layer)
    epsg, proj_bbox = _pick_projection(resolved_bbox, projection)

    await ctx.report_progress(2, 5, f"Fetching imagery for {date_before}...")
    await ctx.report_progress(3, 5, f"Fetching imagery for {date_after}...")
    composite_bytes = await client.compare_dates(
        layer_id, date_before, date_after, proj_bbox,
        image_format=mime_type, epsg=epsg,
    )

    description = (
        f"{layer.title} — comparison\n"
        f"Left: {date_before} | Right: {date_after}\n"
        f"Region: {place or resolved_bbox.wms_bbox}"
    )

    await ctx.report_progress(5, 5, "Complete")
    return [description, Image(data=composite_bytes, format=short_fmt)]


@mcp.tool(
    description="Overlay multiple GIBS layers into a single composite image. "
    "WMS supports up to 5 layers composited together."
)
async def get_imagery_composite(
    layer_ids: list[str],
    date: str,
    ctx: Context,
    bbox: list[float] | None = None,
    place: str | None = None,
    width: int = 1024,
    height: int = 1024,
    projection: str = "auto",
):
    """Multi-layer composite image.

    Args:
        layer_ids: List of GIBS layer identifiers to overlay (max 5).
        date: Date in YYYY-MM-DD format.
        bbox: Bounding box as [west, south, east, north].
        place: Place name to geocode.
        width: Image width in pixels.
        height: Image height in pixels.
        projection: EPSG code or "auto" to detect from bbox.
    """
    client = _get_client()

    if len(layer_ids) > 5:
        return "WMS supports at most 5 layers per composite."

    await ctx.report_progress(1, 3, "Resolving location...")
    try:
        resolved_bbox = await _resolve_bbox(client, bbox, place)
    except Exception as exc:
        return str(exc)

    epsg, proj_bbox = _pick_projection(resolved_bbox, projection)

    layer_names = ", ".join(layer_ids)
    await ctx.report_progress(2, 3, f"Fetching {len(layer_ids)}-layer composite...")
    image_bytes = await client.get_wms_composite(
        layer_ids, date, proj_bbox, width, height, epsg=epsg,
    )

    description = (
        f"Composite: {layer_names}\nDate: {date}\nRegion: {place or resolved_bbox.wms_bbox}"
    )

    await ctx.report_progress(3, 3, "Complete")
    return [description, Image(data=image_bytes, format="jpeg")]


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  TOOLS — Interpretation
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


@mcp.tool(
    description="Explain what the colors in a GIBS layer mean. "
    "Returns a natural-language description mapping colors to scientific values and units."
)
async def explain_layer_colormap(layer_id: str, ctx: Context) -> str:
    """Get natural-language explanation of a layer's colormap.

    Args:
        layer_id: GIBS layer identifier.
    """
    client = _get_client()
    await ctx.report_progress(1, 2, "Fetching colormap...")
    result = await client.explain_layer_colormap(layer_id)
    await ctx.report_progress(2, 2, "Complete")
    return result


@mcp.tool(description="Fetch the pre-rendered legend image for a GIBS layer.")
async def get_legend(
    layer_id: str,
    ctx: Context,
    orientation: str = "horizontal",
):
    """Fetch the legend graphic for a layer.

    Args:
        layer_id: GIBS layer identifier.
        orientation: "horizontal" or "vertical".
    """
    client = _get_client()
    await ctx.report_progress(1, 2, "Fetching legend...")
    legend_bytes = await client.get_legend_image(layer_id, orientation)

    if legend_bytes is None:
        return f"No legend available for '{layer_id}'."

    await ctx.report_progress(2, 2, "Complete")
    return [f"Legend for {layer_id}", Image(data=legend_bytes, format="png")]


@mcp.tool(
    description="Get the exact data value at a geographic coordinate. "
    "Reverse-maps the pixel color through the layer's colormap to recover "
    "the numeric value with units. Only works for layers with colormaps."
)
async def query_point(
    layer_id: str,
    date: str,
    lat: float,
    lon: float,
    ctx: Context,
) -> str:
    """Query the data value at a specific point.

    Args:
        layer_id: GIBS layer identifier (must have a colormap).
        date: Date in YYYY-MM-DD format.
        lat: Latitude in decimal degrees.
        lon: Longitude in decimal degrees.
    """
    client = _get_client()

    layer = client.get_layer(layer_id)
    if layer is None:
        return f"Layer '{layer_id}' not found."
    if not layer.has_colormap:
        return f"Layer '{layer_id}' has no colormap — cannot reverse-map pixel values."

    await ctx.report_progress(1, 3, "Fetching pixel...")
    result = await client.query_point(layer_id, date, lat, lon)
    await ctx.report_progress(3, 3, "Complete")

    return json.dumps(result, indent=2)


@mcp.tool(
    description="Fetch imagery for a layer across multiple dates. "
    "Returns a sequence of images at regular intervals between start and end dates. "
    "Useful for observing temporal progression (up to 12 frames)."
)
async def get_time_series(
    layer_id: str,
    start_date: str,
    end_date: str,
    ctx: Context,
    bbox: list[float] | None = None,
    place: str | None = None,
    steps: int = 6,
    width: int = 512,
    height: int = 512,
    projection: str = "auto",
):
    """Fetch imagery for multiple dates in a range.

    Args:
        layer_id: GIBS layer identifier.
        start_date: First date (YYYY-MM-DD).
        end_date: Last date (YYYY-MM-DD).
        bbox: Bounding box as [west, south, east, north].
        place: Place name to geocode.
        steps: Number of evenly-spaced frames (2-12).
        width: Image width in pixels.
        height: Image height in pixels.
        projection: EPSG code or "auto" to detect from bbox.
    """
    client = _get_client()

    layer = client.get_layer(layer_id)
    if layer is None:
        return f"Layer '{layer_id}' not found."

    steps = max(2, min(steps, 12))
    dates = _generate_dates(start_date, end_date, steps)

    await ctx.report_progress(1, steps + 2, "Resolving location...")
    try:
        resolved_bbox = await _resolve_bbox(client, bbox, place)
    except Exception as exc:
        return str(exc)

    mime_type, short_fmt = _pick_format(layer)
    epsg, proj_bbox = _pick_projection(resolved_bbox, projection)

    results = []
    for i, date in enumerate(dates):
        await ctx.report_progress(
            i + 2, steps + 2, f"Fetching {date} ({i + 1}/{len(dates)})..."
        )
        image_bytes = await client.get_wms_image(
            layer_id, date, proj_bbox, width, height, mime_type, epsg
        )
        results.append(f"{layer.title} — {date}")
        results.append(Image(data=image_bytes, format=short_fmt))

    await ctx.report_progress(steps + 2, steps + 2, "Complete")
    return results


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  TOOLS — Utility
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


@mcp.tool(
    description="Geocode a place name to geographic coordinates and bounding box. "
    "Uses OpenStreetMap Nominatim."
)
async def resolve_place(place: str, ctx: Context) -> str:
    """Resolve a place name to coordinates and bounding box.

    Args:
        place: Place name (e.g. "Tokyo", "California", "Great Barrier Reef").
    """
    client = _get_client()
    await ctx.report_progress(1, 2, "Geocoding...")
    result = await client.resolve_place(place)

    if result is None:
        return f"Could not geocode '{place}'. Try a more specific name."

    await ctx.report_progress(2, 2, "Complete")
    return json.dumps(
        {
            "display_name": result.display_name,
            "lat": result.lat,
            "lon": result.lon,
            "bbox": {
                "west": result.bbox.west,
                "south": result.bbox.south,
                "east": result.bbox.east,
                "north": result.bbox.north,
            },
        },
        indent=2,
    )


@mcp.tool(
    description="Construct a direct WMTS tile URL for a specific "
    "layer, date, zoom level, and tile coordinates."
)
async def build_tile_url(
    layer_id: str,
    date: str,
    zoom: int,
    row: int,
    col: int,
    projection: str = "4326",
) -> str:
    """Build a WMTS REST tile URL.

    Args:
        layer_id: GIBS layer identifier.
        date: Date in YYYY-MM-DD format.
        zoom: Zoom level (0-12 depending on resolution).
        row: Tile row.
        col: Tile column.
        projection: EPSG code ("4326", "3857", "3413", "3031").
    """
    client = _get_client()
    layer = client.get_layer(layer_id)

    ext = "jpg"
    tile_matrix_set = "250m"
    if layer:
        if layer.formats and "image/png" in layer.formats:
            ext = "png"
        if layer.tile_matrix_sets:
            tile_matrix_set = layer.tile_matrix_sets[0]

    url = client.build_tile_url(
        layer_id,
        date,
        zoom,
        row,
        col,
        tile_matrix_set,
        ext,
        projection,
    )
    return url


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  RESOURCES
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


@mcp.resource("gibs://catalog")
async def catalog_resource() -> str:
    """Full GIBS layer catalog grouped by measurement category."""
    client = _get_client()

    by_measurement: dict[str, list[dict]] = {}
    for layer in client.layer_index.values():
        key = layer.measurement or "Unknown"
        if key not in by_measurement:
            by_measurement[key] = []
        by_measurement[key].append(
            {
                "id": layer.identifier,
                "title": layer.title,
                "has_colormap": layer.has_colormap,
            }
        )

    return json.dumps(by_measurement, indent=2)


@mcp.resource("gibs://layer/{layer_id}")
async def layer_resource(layer_id: str) -> str:
    """Individual layer metadata as JSON."""
    client = _get_client()
    layer = client.get_layer(layer_id)
    if layer is None:
        return json.dumps({"error": f"Layer '{layer_id}' not found"})

    await client.fetch_layer_metadata(layer_id)
    return layer.model_dump_json(indent=2)


@mcp.resource("gibs://colormap/{layer_id}")
async def colormap_resource(layer_id: str) -> str:
    """Natural-language explanation of a layer's colormap."""
    client = _get_client()
    return await client.explain_layer_colormap(layer_id)


@mcp.resource("gibs://dates/{layer_id}")
async def dates_resource(layer_id: str) -> str:
    """Available date range for a layer."""
    client = _get_client()
    layer = client.get_layer(layer_id)
    if layer is None:
        return json.dumps({"error": f"Layer '{layer_id}' not found"})

    info: dict = {}
    if layer.time:
        info["start"] = layer.time.start
        info["end"] = layer.time.end
        info["period"] = layer.time.period
        info["default"] = layer.time.default

    try:
        domains = await client.describe_domains(layer_id)
        if "time_domain" in domains:
            info["live_time_domain"] = domains["time_domain"]
    except (httpx.HTTPError, RuntimeError):
        pass

    return json.dumps(info, indent=2)


@mcp.resource("gibs://projections")
async def projections_resource() -> str:
    """Available GIBS projections with endpoint information."""
    return json.dumps(PROJECTIONS, indent=2)


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  PROMPTS
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


@mcp.prompt
def investigate_event(
    event_type: str,
    location: str,
    date: str,
) -> str:
    """Guided workflow for investigating a natural event with GIBS imagery.

    Args:
        event_type: Type of event (e.g. "wildfire", "hurricane", "flood", "volcanic eruption").
        location: Where the event occurred.
        date: Approximate date of the event (YYYY-MM-DD).
    """
    lines = [
        f"Investigate the {event_type} near {location} "
        f"around {date} using NASA GIBS satellite imagery.",
        "",
        "Follow this workflow:",
        "",
        f"1. **Find relevant layers**: Search for layers related to "
        f'"{event_type}" (e.g. fire layers for wildfires, '
        f'precipitation for floods). Also search for "true color" '
        f'or "corrected reflectance" for visual context.',
        "",
        f"2. **Verify dates**: Check that the layers have data "
        f"available for {date}. Adjust the date if needed.",
        "",
        f'3. **Get imagery**: Fetch imagery for "{location}" on '
        f"{date}. Use the true color layer first for visual context.",
        "",
        "4. **Compare before/after**: Get imagery from a few days "
        "or weeks before the event and compare with imagery "
        "during/after to show the impact.",
        "",
        "5. **Explain the data**: If the layers have colormaps, use "
        "explain_colormap to understand what the colors represent.",
        "",
        f"6. **Summarize findings**: Describe what the satellite "
        f"data reveals about the {event_type} — extent, intensity, "
        f"progression, or impact visible from space.",
    ]
    return "\n".join(lines)


@mcp.prompt
def earth_overview() -> str:
    """Introduction to GIBS and suggested explorations."""
    lines = [
        "You have access to NASA's Global Imagery Browse Services "
        "(GIBS) through the mcgibs MCP server. GIBS provides 1000+ "
        "satellite visualization layers covering the entire Earth.",
        "",
        "Here are some things you can explore:",
        "",
        '**Recent events**: Search for "fire", "aerosol", or '
        '"flood" layers to investigate recent natural events.',
        "",
        '**Climate data**: Search for "temperature", "sea ice", '
        '"snow cover", or "vegetation" to explore climate '
        "indicators.",
        "",
        "**True color imagery**: The "
        '"MODIS_Terra_CorrectedReflectance_TrueColor" layer '
        "provides daily true-color Earth imagery — great for "
        "seeing cloud patterns, smoke plumes, or algal blooms.",
        "",
        "**Time comparisons**: Use compare_dates to see how a "
        "region has changed over time — glacier retreat, urban "
        "expansion, seasonal changes.",
        "",
        "**Understanding the data**: Use explain_colormap on any "
        "scientific layer to understand what the colors represent "
        "in physical units.",
        "",
        "Start by searching for a topic that interests you, "
        "or ask about a specific location and date!",
    ]
    return "\n".join(lines)


@mcp.prompt
def satellite_snapshot(
    place: str,
    date: str = "today",
) -> str:
    """Quick satellite view of any place on Earth.

    Args:
        place: Location to view (e.g. "Tokyo", "Amazon Rainforest", "Sahara Desert").
        date: Date to view (YYYY-MM-DD), or "today" for the most recent imagery.
    """
    lines = [
        f"Show me a satellite view of {place} on {date}.",
        "",
        "Follow this workflow:",
        "",
        f'1. **Geocode**: Use resolve_place to get coordinates for "{place}".',
        "",
        f"2. **True color first**: Fetch imagery for "
        f'"MODIS_Terra_CorrectedReflectance_TrueColor" '
        f"on {date} at that location. This gives a natural-looking view.",
        "",
        f"3. **Find science layers**: Search for layers relevant to "
        f'"{place}" — consider what would be scientifically interesting '
        f"for this region (vegetation for forests, sea surface temperature "
        f"for coastal areas, snow cover for mountains, etc.).",
        "",
        "4. **Check dates**: Verify the science layers have data for "
        f"{date} using check_layer_dates.",
        "",
        "5. **Overlay**: If a relevant science layer is available, "
        "use get_imagery_composite to overlay it on the true color base.",
        "",
        "6. **Interpret**: If the overlaid layer has a colormap, "
        "use explain_layer_colormap to describe what the colors mean.",
        "",
        f"7. **Describe**: Summarize what's visible in the satellite view "
        f"of {place} — notable features, weather patterns, land use, "
        f"or any phenomena visible from orbit.",
    ]
    return "\n".join(lines)


@mcp.prompt
def climate_monitor(
    indicator: str,
    location: str,
    start_date: str,
    end_date: str,
) -> str:
    """Track a climate indicator over time at a specific location.

    Args:
        indicator: What to monitor (e.g. "sea ice", "vegetation", "temperature", "snow cover").
        location: Region to monitor (e.g. "Arctic", "Amazon Basin", "Greenland").
        start_date: Beginning of monitoring period (YYYY-MM-DD).
        end_date: End of monitoring period (YYYY-MM-DD).
    """
    lines = [
        f"Monitor {indicator} changes near {location} "
        f"from {start_date} to {end_date}.",
        "",
        "Follow this workflow:",
        "",
        f'1. **Find layers**: Search for GIBS layers related to "{indicator}". '
        f"Look for layers with colormaps — these encode quantitative data.",
        "",
        "2. **Check availability**: Use check_layer_dates on the best "
        f"matching layer to confirm data exists for both {start_date} "
        f"and {end_date}.",
        "",
        "3. **Understand the scale**: Use explain_layer_colormap to learn "
        "what the color values represent (units, range). "
        "Also fetch the legend with get_legend for visual reference.",
        "",
        f"4. **Compare endpoints**: Use compare_dates to create a "
        f"side-by-side view of {start_date} vs {end_date} "
        f'for "{location}". This immediately shows the magnitude of change.',
        "",
        "5. **Intermediate snapshots**: Fetch 1-2 additional dates between "
        "start and end using get_imagery to show the progression "
        "(e.g. mid-point of the range).",
        "",
        "6. **Synthesize**: Describe the observed trend, referencing "
        "specific colormap values where possible. Is the indicator "
        "increasing, decreasing, or showing seasonal variation? "
        "What does the spatial pattern reveal?",
    ]
    return "\n".join(lines)


@mcp.prompt
def layer_deep_dive(
    layer_id: str,
    location: str = "global",
    date: str = "",
) -> str:
    """Full scientific analysis of a single GIBS layer.

    Args:
        layer_id: The GIBS layer identifier (e.g. "AIRS_L3_Surface_Air_Temperature_Daily_Day").
        location: Region to focus on, or "global" for worldwide view.
        date: Specific date (YYYY-MM-DD), or empty for the most recent available.
    """
    location_desc = f'for "{location}"' if location != "global" else "at global scale"
    date_desc = f"on {date}" if date else "using the most recent available date"

    lines = [
        f"Do a deep scientific analysis of the {layer_id} layer "
        f"{location_desc} {date_desc}.",
        "",
        "Follow this workflow:",
        "",
        "1. **Layer metadata**: Use get_layer_info to retrieve full metadata "
        f'for "{layer_id}" — instrument, platform, resolution, date range, '
        "and scientific description.",
        "",
        "2. **Color interpretation**: Use explain_layer_colormap to get a "
        "detailed description of what each color represents, including "
        "physical units and value ranges. Fetch the legend with get_legend.",
        "",
        f"3. **Sample imagery**: Use get_imagery to fetch the layer "
        f"{location_desc} {date_desc}. If no date was specified, "
        "use check_layer_dates to find the latest available date first.",
        "",
        "4. **Image interpretation**: Walk through what's visible in the "
        "image, referencing specific colormap values. Identify notable "
        "spatial patterns, anomalies, or features.",
        "",
        "5. **Scientific context**: Explain what this layer measures, "
        "why it matters, what instrument collects the data, and how "
        "scientists use it. Connect the visualization to the underlying "
        "Earth science.",
    ]
    return "\n".join(lines)


@mcp.prompt
def multi_layer_story(
    topic: str,
    location: str,
    date: str,
) -> str:
    """Compose a data-driven narrative by combining multiple GIBS layers.

    Args:
        topic: Story theme (e.g. "wildfire smoke", "ocean productivity", "dust storm").
        location: Region of interest.
        date: Date for the story (YYYY-MM-DD).
    """
    lines = [
        f'Tell the story of "{topic}" near {location} on {date} '
        f"by compositing multiple satellite layers.",
        "",
        "Follow this workflow:",
        "",
        f'1. **Find related layers**: Search for 2-4 layers related to "{topic}". '
        f"Choose layers that complement each other — for example, "
        f"fire detections + aerosol optical depth + true color for wildfires, "
        f"or SST + chlorophyll + true color for ocean events.",
        "",
        "2. **Verify dates**: Use check_layer_dates on each candidate "
        f"to confirm data is available for {date}.",
        "",
        "3. **Understand each layer**: For each layer with a colormap, "
        "use explain_layer_colormap to understand the value encoding.",
        "",
        "4. **Composite view**: Use get_imagery_composite to overlay "
        "the layers into a single image. Put the base layer "
        "(true color or reference) first in the list.",
        "",
        "5. **Individual layers**: Also fetch get_imagery for each layer "
        "separately so you can describe what each one contributes.",
        "",
        "6. **Narrative**: Write a data-journalism style narrative "
        "describing how the layers interact and what story they tell "
        "together. Reference specific colormap values and spatial "
        "patterns. Explain cause and effect where the data supports it.",
    ]
    return "\n".join(lines)


@mcp.prompt
def polar_watch(
    pole: str,
    date: str,
    compare_date: str = "",
) -> str:
    """Monitor Arctic or Antarctic ice and snow conditions.

    Args:
        pole: Which pole — "arctic" or "antarctic".
        date: Primary observation date (YYYY-MM-DD).
        compare_date: Optional second date for before/after comparison (YYYY-MM-DD).
    """
    if pole.lower().startswith("ant"):
        region = "Antarctic"
        bbox = "[-180, -90, 180, -60]"
        projection = "3031"
    else:
        region = "Arctic"
        bbox = "[-180, 60, 180, 90]"
        projection = "3413"

    lines = [
        f"Monitor {region} ice and snow conditions on {date}.",
        "",
        "Follow this workflow:",
        "",
        f'1. **Find polar layers**: Search for "sea ice", "snow", '
        f'and "ice sheet" layers. Focus on layers that cover the '
        f"{region} region.",
        "",
        f"2. **Check availability**: Use check_layer_dates to verify "
        f"data exists for {date} on the selected layers.",
        "",
        f"3. **Polar imagery**: Fetch imagery using the {region} "
        f"bounding box {bbox} with projection=\"{projection}\". "
        f"This polar stereographic projection shows the region properly "
        f"centered on the pole. Get true color first, then ice/snow "
        f"concentration layers.",
        "",
        "4. **Color interpretation**: Use explain_layer_colormap on each "
        "science layer and fetch legends with get_legend to understand "
        "concentration values and classifications.",
        "",
        "5. **Embeddable tiles**: Use build_tile_url to generate a "
        "WMTS URL for the primary ice layer — useful for embedding "
        "in reports or dashboards.",
    ]

    if compare_date:
        lines.extend([
            "",
            f"6. **Temporal comparison**: Use compare_dates to show "
            f"{compare_date} vs {date} side-by-side. Describe the "
            f"change in ice extent and concentration between these dates.",
        ])
    else:
        lines.extend([
            "",
            f"6. **Summarize**: Describe the current {region} ice/snow "
            f"state visible in the imagery, referencing colormap values "
            f"for concentration percentages.",
        ])

    return "\n".join(lines)


@mcp.prompt
def quantitative_snapshot(
    layer_id: str,
    locations: str,
    date: str,
) -> str:
    """Query exact data values at specific geographic coordinates.

    Args:
        layer_id: GIBS layer identifier with a colormap
            (e.g. "AIRS_L3_Surface_Air_Temperature_Daily_Day").
        locations: Comma-separated place names (e.g. "Tokyo, Sydney, Sahara Desert").
        date: Date to query (YYYY-MM-DD).
    """
    location_list = [loc.strip() for loc in locations.split(",")]

    lines = [
        f"Query the exact {layer_id} values at these locations on {date}: "
        f"{', '.join(location_list)}.",
        "",
        "Follow this workflow:",
        "",
        f"1. **Understand the scale**: Use explain_layer_colormap on "
        f'"{layer_id}" to learn the value range, units, and what '
        f"different colors represent. Fetch the legend with get_legend.",
        "",
        "2. **Resolve coordinates**: Use resolve_place for each location "
        "to get precise lat/lon coordinates.",
        "",
        "3. **Query each point**: Use query_point for each location "
        "with the resolved coordinates. This returns the exact data "
        "value by reverse-mapping the pixel color through the colormap.",
        "",
        "4. **Compare and contextualize**: Present the results in a table "
        "with location, value (in native and converted units if applicable), "
        "and a qualitative description. Explain why the values differ — "
        "latitude, altitude, season, land use, proximity to water, etc.",
        "",
        "5. **Verify with imagery**: Optionally fetch get_imagery for one "
        "location to visually confirm the data pattern matches the "
        "point query results.",
    ]
    return "\n".join(lines)


@mcp.prompt
def seasonal_timelapse(
    layer_id: str,
    location: str,
    start_date: str,
    end_date: str,
) -> str:
    """Watch a location change across multiple dates.

    Args:
        layer_id: GIBS layer identifier (e.g. "MODIS_Terra_NDVI_8Day").
        location: Place to observe.
        start_date: Beginning of the period (YYYY-MM-DD).
        end_date: End of the period (YYYY-MM-DD).
    """
    lines = [
        f"Show how {layer_id} changes at {location} "
        f"from {start_date} to {end_date}.",
        "",
        "Follow this workflow:",
        "",
        f"1. **Layer info**: Use get_layer_info on \"{layer_id}\" to "
        f"understand what it measures, its temporal period, and resolution.",
        "",
        "2. **Check dates**: Use check_layer_dates to verify data "
        f"covers {start_date} through {end_date}.",
        "",
        "3. **Color interpretation**: Use explain_layer_colormap to "
        "understand the value encoding, so you can describe changes "
        "in scientific terms rather than just color names.",
        "",
        f"4. **Time series**: Use get_time_series to fetch imagery "
        f'for "{location}" from {start_date} to {end_date}. '
        f"This returns a sequence of images at regular intervals.",
        "",
        "5. **Narrate the progression**: For each frame, describe "
        "what's visible and how it differs from the previous frame. "
        "Reference colormap values to quantify the change. "
        "Identify the overall trend and any notable transitions.",
        "",
        "6. **Bookend comparison**: Use compare_dates with "
        f"{start_date} and {end_date} for a direct side-by-side "
        "showing the total change across the period.",
    ]
    return "\n".join(lines)


# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#  Entry point
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


def main():
    mcp.run()
