/**
 * 地図・位置情報ツールモジュール
 *
 * - SearchPlaces  : 地名・住所・施設名を検索（Nominatim / Google Places API）
 * - GetDirections : 2地点間の経路・距離・所要時間を取得（OSRM / Google Directions API）
 * - ReverseGeocode: 緯度経度から住所を取得（Nominatim）
 *
 * Google Maps API キー（tools.google_maps_api_key）が設定されている場合は
 * Google Maps API を優先使用し、設定されていない場合は無料の OSS サービスを使用する。
 */

import { ToolDef } from '../../llm/openai-compat.js';
import { ToolContext, ToolResult } from './core.js';
import { logger } from '../../logger.js';
import { writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';
import type { StructuredBlock, MapPlaceItem } from './structured-blocks.js';

// -----------------------------------------------------------------------
// ツール定義
// -----------------------------------------------------------------------

export const TOOL_DEFS: Record<string, ToolDef> = {
  SearchPlaces: {
    type: 'function',
    function: {
      name: 'SearchPlaces',
      description: '地名・住所・施設を検索し座標・住所・詳細を返す（リッチUIで地図表示）。詳細は ReadToolDoc({ name: "SearchPlaces" })。',
      parameters: {
        type: 'object',
        properties: {
          query: {
            type: 'string',
            description: '検索クエリ（地名、施設名、住所など）',
          },
          lang: {
            type: 'string',
            description: '言語コード（例: ja, en）。省略時は ja を使用',
          },
          limit: {
            type: 'number',
            description: '最大取得件数（1〜20、省略時は5）',
          },
        },
        required: ['query'],
      },
    },
  },

  GetDirections: {
    type: 'function',
    function: {
      name: 'GetDirections',
      description: '2地点間の経路・距離・所要時間を取得する（output_html=true で経路HTMLも生成可能）。詳細は ReadToolDoc({ name: "GetDirections" })。',
      parameters: {
        type: 'object',
        properties: {
          origin: {
            type: 'string',
            description: '出発地（住所、地名、または "緯度,経度" 形式）',
          },
          destination: {
            type: 'string',
            description: '目的地（住所、地名、または "緯度,経度" 形式）',
          },
          mode: {
            type: 'string',
            enum: ['driving', 'walking', 'cycling'],
            description: '移動手段（省略時は driving）',
          },
          output_html: {
            type: 'boolean',
            description: 'true にすると output/maps/ に経路HTMLファイルを生成する',
          },
          filename: {
            type: 'string',
            description: 'output_html=true の場合のHTMLファイル名（省略時は自動生成）',
          },
        },
        required: ['origin', 'destination'],
      },
    },
  },

  ReverseGeocode: {
    type: 'function',
    function: {
      name: 'ReverseGeocode',
      description: '緯度経度から住所を取得する（逆ジオコーディング）。詳細は ReadToolDoc({ name: "ReverseGeocode" })。',
      parameters: {
        type: 'object',
        properties: {
          lat: {
            type: 'number',
            description: '緯度',
          },
          lon: {
            type: 'number',
            description: '経度',
          },
          lang: {
            type: 'string',
            description: '言語コード（例: ja, en）。省略時は ja を使用',
          },
        },
        required: ['lat', 'lon'],
      },
    },
  },
};

// -----------------------------------------------------------------------
// 型定義
// -----------------------------------------------------------------------

interface NominatimAddress {
  shop?: string;
  amenity?: string;
  building?: string;
  house_number?: string;
  road?: string;
  neighbourhood?: string;
  suburb?: string;
  city?: string;
  town?: string;
  village?: string;
  county?: string;
  state?: string;
  country?: string;
  postcode?: string;
  country_code?: string;
}

interface NominatimSearchResult {
  place_id: number;
  lat: string;
  lon: string;
  display_name: string;
  address: NominatimAddress;
  type: string;
  class: string;
  importance: number;
}

interface NominatimReverseResult {
  place_id: number;
  lat: string;
  lon: string;
  display_name: string;
  address: NominatimAddress;
  boundingbox: string[];
}

interface PlaceInfo {
  name: string;
  address: string;
  lat: number;
  lon: number;
  type: string;
  details: string;
}

interface RouteInfo {
  distance: string;
  duration: string;
  steps: string[];
  originCoords: { lat: number; lon: number };
  destCoords: { lat: number; lon: number };
  originName: string;
  destName: string;
}

// -----------------------------------------------------------------------
// 定数
// -----------------------------------------------------------------------

const NOMINATIM_BASE = 'https://nominatim.openstreetmap.org';
const OSRM_BASE = 'https://router.project-osrm.org';
const GOOGLE_MAPS_BASE = 'https://maps.googleapis.com/maps/api';

// -----------------------------------------------------------------------
// ユーティリティ
// -----------------------------------------------------------------------

function sanitizeFilename(s: string): string {
  return s.replace(/[^a-zA-Z0-9]/g, '_').slice(0, 40);
}

function getApiKey(ctx: ToolContext): string | undefined {
  return ctx.toolsConfig?.googleMapsApiKey;
}

function getTimeoutMs(ctx: ToolContext): number {
  return (ctx.toolsConfig?.mapsTimeout ?? 30) * 1000;
}

async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const res = await fetch(url, {
      signal: controller.signal,
      headers: {
        'User-Agent': 'maestro/1.0 (maps-tool; contact: maestro)',
        'Accept-Language': 'ja,en;q=0.9',
      },
    });
    return res;
  } finally {
    clearTimeout(timer);
  }
}

/**
 * 住所文字列または "lat,lon" 形式の文字列を座標に変換する。
 */
async function geocodeAddress(
  address: string,
  timeout: number,
): Promise<{ lat: number; lon: number } | null> {
  const latLonMatch = address.match(/^(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)$/);
  if (latLonMatch) {
    return { lat: parseFloat(latLonMatch[1]!), lon: parseFloat(latLonMatch[2]!) };
  }

  const url =
    `${NOMINATIM_BASE}/search?q=${encodeURIComponent(address)}&format=json&limit=1&accept-language=ja`;
  try {
    const res = await fetchWithTimeout(url, timeout);
    if (!res.ok) return null;
    const data = (await res.json()) as NominatimSearchResult[];
    if (!data.length) return null;
    return { lat: parseFloat(data[0]!.lat), lon: parseFloat(data[0]!.lon) };
  } catch {
    return null;
  }
}

function ensureMapsDir(workspacePath: string): string {
  const dir = join(workspacePath, 'output', 'maps');
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  return dir;
}

// -----------------------------------------------------------------------
// HTML生成
// -----------------------------------------------------------------------

function generateDirectionsHtml(
  origin: { name: string; lat: number; lon: number },
  dest: { name: string; lat: number; lon: number },
  route: { distance: string; duration: string; steps: string[] },
): string {
  const centerLat = (origin.lat + dest.lat) / 2;
  const centerLon = (origin.lon + dest.lon) / 2;

  const stepsHtml =
    route.steps.length > 0
      ? `<h4>経路ステップ</h4><ol>${route.steps.map((s) => `<li>${escHtml(s)}</li>`).join('')}</ol>`
      : '';

  const osmUrl =
    `https://www.openstreetmap.org/directions?from=${origin.lat},${origin.lon}&to=${dest.lat},${dest.lon}`;

  return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>経路: ${escHtml(origin.name)} → ${escHtml(dest.name)}</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: sans-serif; }
  #map { height: 480px; }
  #info { padding: 12px; line-height: 1.8; }
  h3, h4 { margin: 8px 0 4px; }
  ol { padding-left: 20px; }
  li { margin: 4px 0; }
  a { color: #0078d4; }
</style>
</head>
<body>
<div id="map"></div>
<div id="info">
  <h3>経路情報</h3>
  <p>出発地: ${escHtml(origin.name)}</p>
  <p>目的地: ${escHtml(dest.name)}</p>
  <p>距離: <b>${route.distance}</b> / 所要時間: <b>${route.duration}</b></p>
  <p><a href="${osmUrl}" target="_blank">OpenStreetMapで開く</a></p>
  ${stepsHtml}
</div>
<script>
  var map = L.map('map').setView([${centerLat}, ${centerLon}], 10);
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
    maxZoom: 19
  }).addTo(map);
  var originMarker = L.marker([${origin.lat}, ${origin.lon}], {title: "出発地"}).addTo(map)
    .bindPopup("<b>出発地</b><br>${escHtml(origin.name).replace(/"/g, '\\"')}").openPopup();
  var destMarker = L.marker([${dest.lat}, ${dest.lon}], {title: "目的地"}).addTo(map)
    .bindPopup("<b>目的地</b><br>${escHtml(dest.name).replace(/"/g, '\\"')}");
  var group = L.featureGroup([originMarker, destMarker]);
  map.fitBounds(group.getBounds().pad(0.2));
</script>
</body>
</html>`;
}

function escHtml(s: string): string {
  return s
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;');
}

// -----------------------------------------------------------------------
// SearchPlaces
// -----------------------------------------------------------------------

async function executeSearchPlaces(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  const query = typeof input['query'] === 'string' ? input['query'].trim() : '';
  if (!query) return { output: 'query は必須です', isError: true };

  const lang = typeof input['lang'] === 'string' ? input['lang'] : 'ja';
  const limit =
    typeof input['limit'] === 'number' ? Math.min(20, Math.max(1, Math.floor(input['limit']))) : 5;
  const timeout = getTimeoutMs(ctx);
  const apiKey = getApiKey(ctx);

  let places: PlaceInfo[] = [];

  // Google Places API（APIキーがある場合）
  if (apiKey) {
    try {
      const url =
        `${GOOGLE_MAPS_BASE}/place/textsearch/json?query=${encodeURIComponent(query)}&language=${lang}&key=${apiKey}`;
      const res = await fetchWithTimeout(url, timeout);
      if (res.ok) {
        const data = (await res.json()) as {
          status: string;
          results: Array<{
            name: string;
            formatted_address: string;
            geometry: { location: { lat: number; lng: number } };
            types: string[];
            rating?: number;
            opening_hours?: { open_now?: boolean };
          }>;
        };
        if (data.status === 'OK' && data.results.length > 0) {
          places = data.results.slice(0, limit).map((r) => ({
            name: r.name,
            address: r.formatted_address,
            lat: r.geometry.location.lat,
            lon: r.geometry.location.lng,
            type: r.types[0] ?? '',
            details: [
              r.rating !== undefined ? `評価: ${r.rating}` : '',
              r.opening_hours?.open_now !== undefined
                ? r.opening_hours.open_now
                  ? '営業中'
                  : '営業時間外'
                : '',
            ]
              .filter(Boolean)
              .join(', '),
          }));
          logger.debug(`[maps] Google Places API: ${places.length}件取得`);
        }
      }
    } catch (err) {
      logger.warn(`[maps] Google Places API エラー: ${err}`);
    }
  }

  // Nominatim フォールバック
  if (places.length === 0) {
    try {
      const url =
        `${NOMINATIM_BASE}/search?q=${encodeURIComponent(query)}&format=json&limit=${limit}&accept-language=${lang}&addressdetails=1`;
      const res = await fetchWithTimeout(url, timeout);
      if (!res.ok) {
        return { output: `Nominatim API エラー: HTTP ${res.status}`, isError: true };
      }
      const data = (await res.json()) as NominatimSearchResult[];
      places = data.map((r) => ({
        name: r.display_name.split(',')[0]?.trim() ?? r.display_name,
        address: r.display_name,
        lat: parseFloat(r.lat),
        lon: parseFloat(r.lon),
        type: r.type,
        details: '',
      }));
      logger.debug(`[maps] Nominatim: ${places.length}件取得`);
    } catch (err) {
      return {
        output: `地図検索エラー: ${err instanceof Error ? err.message : String(err)}`,
        isError: true,
      };
    }
  }

  if (places.length === 0) {
    return {
      output: `「${query}」に一致する場所が見つかりませんでした。`,
      isError: false,
    };
  }

  let output = `## 地図検索結果: ${query}\n\n`;
  output += `${places.length}件の場所が見つかりました。\n\n`;

  for (const [i, place] of places.entries()) {
    output += `### ${i + 1}. ${place.name}\n`;
    output += `- **住所**: ${place.address}\n`;
    output += `- **座標**: ${place.lat.toFixed(6)}, ${place.lon.toFixed(6)}\n`;
    if (place.type) output += `- **種別**: ${place.type}\n`;
    if (place.details) output += `- **詳細**: ${place.details}\n`;
    output += `- **地図リンク**: https://www.openstreetmap.org/?mlat=${place.lat}&mlon=${place.lon}&zoom=17\n`;
    output += '\n';
  }

  // 構造化データを生成
  const refId = `map-${Date.now()}`;
  const structuredBlocks: StructuredBlock[] = [{
    refId,
    type: 'map_places',
    title: `地図検索結果: 「${query}」`,
    data: {
      query,
      places: places.map((p): MapPlaceItem => ({
        name: p.name,
        address: p.address,
        lat: p.lat,
        lon: p.lon,
        type: p.type,
        details: p.details,
        mapUrl: `https://www.openstreetmap.org/?mlat=${p.lat}&mlon=${p.lon}&zoom=17`,
      })),
    },
  }];

  return { output: `${output}\n\n[[embed:${refId}]]`, isError: false, structuredBlocks };
}

// -----------------------------------------------------------------------
// GetDirections
// -----------------------------------------------------------------------

async function executeGetDirections(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  const origin = typeof input['origin'] === 'string' ? input['origin'].trim() : '';
  const destination = typeof input['destination'] === 'string' ? input['destination'].trim() : '';
  if (!origin) return { output: 'origin は必須です', isError: true };
  if (!destination) return { output: 'destination は必須です', isError: true };

  const mode = typeof input['mode'] === 'string' ? input['mode'] : 'driving';
  const outputHtml = input['output_html'] === true;
  const timeout = getTimeoutMs(ctx);
  const apiKey = getApiKey(ctx);

  let routeInfo: RouteInfo | null = null;

  // Google Maps Directions API（APIキーがある場合）
  if (apiKey) {
    try {
      const url =
        `${GOOGLE_MAPS_BASE}/directions/json?origin=${encodeURIComponent(origin)}&destination=${encodeURIComponent(destination)}&mode=${mode}&language=ja&key=${apiKey}`;
      const res = await fetchWithTimeout(url, timeout);
      if (res.ok) {
        const data = (await res.json()) as {
          status: string;
          routes: Array<{
            legs: Array<{
              distance: { text: string };
              duration: { text: string };
              steps: Array<{
                html_instructions: string;
                distance: { text: string };
              }>;
              start_address: string;
              end_address: string;
              start_location: { lat: number; lng: number };
              end_location: { lat: number; lng: number };
            }>;
          }>;
        };
        if (data.status === 'OK' && data.routes.length > 0) {
          const leg = data.routes[0]!.legs[0]!;
          routeInfo = {
            distance: leg.distance.text,
            duration: leg.duration.text,
            steps: leg.steps.map(
              (s) => `${s.html_instructions.replace(/<[^>]+>/g, '')} (${s.distance.text})`,
            ),
            originCoords: { lat: leg.start_location.lat, lon: leg.start_location.lng },
            destCoords: { lat: leg.end_location.lat, lon: leg.end_location.lng },
            originName: leg.start_address,
            destName: leg.end_address,
          };
          logger.debug(`[maps] Google Directions API: 経路取得成功`);
        }
      }
    } catch (err) {
      logger.warn(`[maps] Google Directions API エラー: ${err}`);
    }
  }

  // OSRM フォールバック
  if (!routeInfo) {
    try {
      const [originCoords, destCoords] = await Promise.all([
        geocodeAddress(origin, timeout),
        geocodeAddress(destination, timeout),
      ]);

      if (!originCoords) {
        return {
          output: `出発地「${origin}」の座標を取得できませんでした。住所を確認してください。`,
          isError: true,
        };
      }
      if (!destCoords) {
        return {
          output: `目的地「${destination}」の座標を取得できませんでした。住所を確認してください。`,
          isError: true,
        };
      }

      // OSRM プロファイルのマッピング
      const osrmProfile = mode === 'walking' ? 'foot' : mode === 'cycling' ? 'bike' : 'car';
      const url =
        `${OSRM_BASE}/route/v1/${osrmProfile}/${originCoords.lon},${originCoords.lat};${destCoords.lon},${destCoords.lat}?overview=false&steps=true`;

      const res = await fetchWithTimeout(url, timeout);
      if (!res.ok) {
        return { output: `OSRM 経路取得エラー: HTTP ${res.status}`, isError: true };
      }

      const data = (await res.json()) as {
        code: string;
        routes: Array<{
          distance: number;
          duration: number;
          legs: Array<{
            steps: Array<{
              maneuver: { type: string; modifier?: string };
              distance: number;
              duration: number;
              name: string;
            }>;
          }>;
        }>;
      };

      if (data.code !== 'Ok' || !data.routes.length) {
        return { output: '経路が見つかりませんでした。', isError: false };
      }

      const route = data.routes[0]!;
      const distanceKm = (route.distance / 1000).toFixed(1);
      const durationMin = Math.round(route.duration / 60);
      const steps =
        route.legs[0]?.steps?.map((s) => {
          const type = s.maneuver.type;
          const modifier = s.maneuver.modifier ? ` ${s.maneuver.modifier}` : '';
          const road = s.name ? ` (${s.name})` : '';
          return `${type}${modifier}${road} — ${Math.round(s.distance)}m`;
        }) ?? [];

      routeInfo = {
        distance: `${distanceKm} km`,
        duration: `${durationMin} 分`,
        steps,
        originCoords,
        destCoords,
        originName: origin,
        destName: destination,
      };
      logger.debug(`[maps] OSRM: 経路取得成功 (${distanceKm}km, ${durationMin}分)`);
    } catch (err) {
      return {
        output: `経路取得エラー: ${err instanceof Error ? err.message : String(err)}`,
        isError: true,
      };
    }
  }

  if (!routeInfo) {
    return { output: '経路情報の取得に失敗しました。', isError: true };
  }

  const modeLabel = mode === 'walking' ? '徒歩' : mode === 'cycling' ? '自転車' : '車';

  let output = `## 経路情報\n\n`;
  output += `- **出発地**: ${routeInfo.originName}\n`;
  output += `- **目的地**: ${routeInfo.destName}\n`;
  output += `- **移動手段**: ${modeLabel}\n`;
  output += `- **距離**: ${routeInfo.distance}\n`;
  output += `- **所要時間**: ${routeInfo.duration}\n`;
  output += `- **地図リンク**: https://www.openstreetmap.org/directions?from=${routeInfo.originCoords.lat},${routeInfo.originCoords.lon}&to=${routeInfo.destCoords.lat},${routeInfo.destCoords.lon}\n\n`;

  if (routeInfo.steps.length > 0) {
    output += `### 経路ステップ\n\n`;
    for (const [i, step] of routeInfo.steps.entries()) {
      output += `${i + 1}. ${step}\n`;
    }
    output += '\n';
  }

  // HTML ファイル生成
  if (outputHtml && ctx.workspacePath) {
    const rawFilename =
      typeof input['filename'] === 'string'
        ? input['filename']
        : `route_${sanitizeFilename(origin)}_to_${sanitizeFilename(destination)}.html`;
    const filename = rawFilename.endsWith('.html') ? rawFilename : `${rawFilename}.html`;
    try {
      const dir = ensureMapsDir(ctx.workspacePath);
      const filePath = join(dir, filename);
      writeFileSync(
        filePath,
        generateDirectionsHtml(
          {
            name: routeInfo.originName,
            lat: routeInfo.originCoords.lat,
            lon: routeInfo.originCoords.lon,
          },
          {
            name: routeInfo.destName,
            lat: routeInfo.destCoords.lat,
            lon: routeInfo.destCoords.lon,
          },
          {
            distance: routeInfo.distance,
            duration: routeInfo.duration,
            steps: routeInfo.steps,
          },
        ),
        'utf-8',
      );
      const relPath = `output/maps/${filename}`;
      output += `**経路プレビュー**: \`${relPath}\`（Leaflet.js によるインタラクティブ地図）\n`;
    } catch (err) {
      output += `（HTMLファイル生成失敗: ${err instanceof Error ? err.message : String(err)}）\n`;
    }
  }

  return { output, isError: false };
}

// -----------------------------------------------------------------------
// ReverseGeocode
// -----------------------------------------------------------------------

async function executeReverseGeocode(
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult> {
  const lat = typeof input['lat'] === 'number' ? input['lat'] : parseFloat(String(input['lat']));
  const lon = typeof input['lon'] === 'number' ? input['lon'] : parseFloat(String(input['lon']));

  if (isNaN(lat) || isNaN(lon)) {
    return { output: 'lat と lon は数値で指定してください', isError: true };
  }
  if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
    return { output: '座標の範囲が無効です（lat: -90〜90, lon: -180〜180）', isError: true };
  }

  const lang = typeof input['lang'] === 'string' ? input['lang'] : 'ja';
  const timeout = getTimeoutMs(ctx);

  try {
    const url =
      `${NOMINATIM_BASE}/reverse?lat=${lat}&lon=${lon}&format=json&accept-language=${lang}&addressdetails=1`;
    const res = await fetchWithTimeout(url, timeout);
    if (!res.ok) {
      return { output: `Nominatim API エラー: HTTP ${res.status}`, isError: true };
    }
    const data = (await res.json()) as NominatimReverseResult;

    if (!data.display_name) {
      return {
        output: `座標 (${lat}, ${lon}) の住所が見つかりませんでした。`,
        isError: false,
      };
    }

    const addr = data.address;

    let output = `## 逆ジオコーディング結果\n\n`;
    output += `**座標**: ${lat}, ${lon}\n\n`;
    output += `**住所（全体）**: ${data.display_name}\n\n`;
    output += `### 住所コンポーネント\n\n`;

    if (addr.postcode) output += `- **郵便番号**: ${addr.postcode}\n`;
    if (addr.country) output += `- **国**: ${addr.country}\n`;
    if (addr.state) output += `- **都道府県**: ${addr.state}\n`;
    if (addr.county) output += `- **郡/区**: ${addr.county}\n`;
    const cityName = addr.city ?? addr.town ?? addr.village;
    if (cityName) output += `- **市区町村**: ${cityName}\n`;
    const districtName = addr.suburb ?? addr.neighbourhood;
    if (districtName) output += `- **地区**: ${districtName}\n`;
    if (addr.road) output += `- **道路/通り**: ${addr.road}\n`;
    if (addr.house_number) output += `- **番地**: ${addr.house_number}\n`;
    if (addr.building) output += `- **建物**: ${addr.building}\n`;
    if (addr.amenity) output += `- **施設**: ${addr.amenity}\n`;
    if (addr.shop) output += `- **店舗**: ${addr.shop}\n`;

    output += `\n**地図リンク**: https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}&zoom=17\n`;

    return { output, isError: false };
  } catch (err) {
    return {
      output: `逆ジオコーディングエラー: ${err instanceof Error ? err.message : String(err)}`,
      isError: true,
    };
  }
}

// -----------------------------------------------------------------------
// エクスポート
// -----------------------------------------------------------------------

export async function executeTool(
  name: string,
  input: Record<string, unknown>,
  ctx: ToolContext,
): Promise<ToolResult | null> {
  switch (name) {
    case 'SearchPlaces':
      return executeSearchPlaces(input, ctx);
    case 'GetDirections':
      return executeGetDirections(input, ctx);
    case 'ReverseGeocode':
      return executeReverseGeocode(input, ctx);
    default:
      return null;
  }
}
