/* eslint-disable max-lines */
import { z } from "zod";
import {
  createDataforseoClient,
  type AdsKeywordItem,
  type KeywordOverviewItem,
} from "@/server/lib/dataforseo";
import { getKeywordDataProvider } from "@/shared/keyword-locations";
import { buildProjectMeta } from "@/server/mcp/context";
import { mcpResponse } from "@/server/mcp/formatters";
import {
  looseObjectOutputSchema,
  optionalMetaOutputSchema,
} from "@/server/mcp/output-schemas";
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
import {
  DEFAULT_LANGUAGE_CODE,
  DEFAULT_LOCATION_CODE,
  languageCodeSchema,
  locationCodeSchema,
  projectIdSchema,
} from "@/server/mcp/schemas";

const rankedResultTypeSchema = z.enum([
  "organic",
  "paid",
  "featured_snippet",
  "local_pack",
  "ai_overview_reference",
]);

const serpCompetitorResultTypeSchema = z.enum([
  "organic",
  "paid",
  "featured_snippet",
  "local_pack",
]);

const marketSchema = z
  .object({
    country: z
      .enum(["US", "USA", "United States", "United States of America"])
      .optional()
      .describe("Country selector. Only the United States is supported."),
  })
  .optional()
  .describe("Optional United States market object. Defaults to United States.");

const nearSchema = z
  .object({
    latitude: z
      .number()
      .min(-90)
      .max(90)
      .describe("Latitude of the search center."),
    longitude: z
      .number()
      .min(-180)
      .max(180)
      .describe("Longitude of the search center."),
    radiusKm: z
      .number()
      .min(1)
      .max(100000)
      .describe("Search radius around the center, in kilometers."),
  })
  .describe("Coordinate and radius to search around.");

const localSerpNearSchema = z
  .object({
    latitude: z
      .number()
      .min(-90)
      .max(90)
      .describe("Latitude the SERP is fetched from."),
    longitude: z
      .number()
      .min(-180)
      .max(180)
      .describe("Longitude the SERP is fetched from."),
    zoom: z
      .number()
      .int()
      .min(4)
      .max(18)
      .optional()
      .describe("Map zoom level (4-18). Higher zoom narrows the local area."),
  })
  .describe("Coordinate (and optional map zoom) the SERP is fetched from.");

const domainTargetSchema = z
  .string()
  .min(1)
  .max(255)
  .refine(
    (value) =>
      /^(?!https?:\/\/)(?!www\.)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i.test(
        value,
      ),
    "Use a domain or subdomain without protocol and without www.",
  );

const rankedTargetSchema = z
  .string()
  .min(1)
  .max(2048)
  .refine(
    (value) =>
      /^https?:\/\/\S+$/.test(value) ||
      domainTargetSchema.safeParse(value).success,
    "Use a domain without protocol/www or an absolute page URL.",
  );

const getRankedKeywordsInputSchema = {
  projectId: projectIdSchema,
  target: rankedTargetSchema.describe(
    "Domain (no protocol/www) or absolute page URL to list ranked keywords for.",
  ),
  market: marketSchema,
  resultTypes: z
    .array(rankedResultTypeSchema)
    .min(1)
    .max(5)
    .optional()
    .describe("SERP result types to include. Defaults to organic and paid."),
  includeSubdomains: z
    .boolean()
    .optional()
    .describe(
      "Include subdomains of the target. Defaults to true for domains, false for page URLs.",
    ),
  minSearchVolume: z
    .number()
    .int()
    .min(0)
    .optional()
    .describe("Only return keywords with at least this monthly search volume."),
  maxRank: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .describe("Only return keywords ranking at this position or better."),
  excludeBrandTerms: z
    .array(z.string().min(1).max(80))
    .min(1)
    .max(10)
    .optional()
    .describe("Exclude keywords containing any of these brand terms."),
  sortBy: z
    .enum(["rank", "search_volume", "traffic_estimate", "cpc"])
    .optional()
    .describe("Sort order for returned rows. Defaults to search_volume."),
  limit: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .describe("Maximum rows to return (1-100). Defaults to 50."),
  offset: z
    .number()
    .int()
    .min(0)
    .max(1000)
    .optional()
    .describe("Rows to skip for pagination."),
} as const;

const searchLocalBusinessesInputSchema = {
  projectId: projectIdSchema,
  query: z
    .string()
    .min(1)
    .max(200)
    .optional()
    .describe("Business name or title text to match."),
  near: nearSchema,
  categories: z
    .array(z.string().min(1).max(120))
    .min(1)
    .max(10)
    .optional()
    .describe("Business categories to filter by (e.g. 'pizza_restaurant')."),
  limit: z
    .number()
    .int()
    .min(1)
    .max(50)
    .optional()
    .describe("Maximum businesses to return (1-50). Defaults to 20."),
} as const;

const localSearchTypeSchema = z.enum(["maps", "local_finder"]);

const getLocalSerpResultsInputSchema = {
  projectId: projectIdSchema,
  keyword: z
    .string()
    .min(1)
    .max(120)
    .describe("Search query to run on Google Maps or Local Finder."),
  near: localSerpNearSchema,
  searchType: localSearchTypeSchema
    .optional()
    .describe("Which local SERP to fetch. Defaults to maps."),
  device: z
    .enum(["desktop", "mobile"])
    .optional()
    .describe("Device the SERP is rendered for. Defaults to desktop."),
  depth: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .describe("Number of results to fetch (1-100). Defaults to 20."),
  languageCode: languageCodeSchema.optional(),
} as const;

const getGoogleBusinessQuestionsInputSchema = {
  projectId: projectIdSchema,
  keyword: z
    .string()
    .min(1)
    .max(200)
    .describe(
      "Business name or search phrase identifying the Google Business Profile.",
    ),
  near: nearSchema,
  depth: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .describe("Maximum Q&A rows to fetch (1-100). Defaults to 20."),
  languageCode: languageCodeSchema.optional(),
} as const;

const findSerpCompetitorsInputSchema = {
  projectId: projectIdSchema,
  keywords: z
    .array(z.string().min(1).max(120))
    .min(1)
    .max(100)
    .describe("Keywords whose SERPs are compared (1-100)."),
  market: marketSchema,
  resultTypes: z
    .array(serpCompetitorResultTypeSchema)
    .min(1)
    .max(4)
    .optional()
    .describe(
      "SERP result types to include. Defaults to organic and local_pack.",
    ),
  excludeDomains: z
    .array(domainTargetSchema)
    .min(1)
    .max(50)
    .optional()
    .describe("Domains to exclude from results (e.g. the user's own site)."),
  includeSubdomains: z
    .boolean()
    .optional()
    .describe("Count subdomains as part of the same competitor domain."),
  sortBy: z
    .enum(["visibility", "traffic_estimate", "avg_position", "keyword_count"])
    .optional()
    .describe("Sort order for returned competitors. Defaults to visibility."),
  limit: z
    .number()
    .int()
    .min(1)
    .max(100)
    .optional()
    .describe("Maximum competitors to return (1-100). Defaults to 50."),
  offset: z
    .number()
    .int()
    .min(0)
    .max(1000)
    .optional()
    .describe("Rows to skip for pagination."),
} as const;

const keywordMetricsSortSchema = z.enum([
  "search_volume",
  "keyword_difficulty",
  "cpc",
  "competition",
]);

const getKeywordMetricsInputSchema = {
  projectId: projectIdSchema,
  keywords: z
    .array(z.string().min(1).max(80))
    .min(1)
    .max(700)
    .describe("Keywords to fetch metrics for (1-700)."),
  locationCode: locationCodeSchema.optional(),
  languageCode: languageCodeSchema.optional(),
  includeMonthlyTrends: z
    .boolean()
    .optional()
    .describe("Include monthly search-volume trend rows. Defaults to true."),
  includeClickstreamData: z
    .boolean()
    .optional()
    .describe(
      "Refine search volumes with clickstream data, which disaggregates Google Ads' grouped close-variant volumes (plurals/misspellings). DOUBLES the credit cost of the call. Default false. No effect for countries served from Google Ads data.",
    ),
  sortBy: keywordMetricsSortSchema
    .optional()
    .describe("Sort order for returned rows. Defaults to search_volume."),
} as const;

type Market = z.infer<typeof marketSchema>;
type GetRankedKeywordsArgs = z.infer<
  z.ZodObject<typeof getRankedKeywordsInputSchema>
>;
type FindSerpCompetitorsArgs = z.infer<
  z.ZodObject<typeof findSerpCompetitorsInputSchema>
>;
type GetKeywordMetricsArgs = z.infer<
  z.ZodObject<typeof getKeywordMetricsInputSchema>
>;
type SearchLocalBusinessesArgs = z.infer<
  z.ZodObject<typeof searchLocalBusinessesInputSchema>
>;
type GetLocalSerpResultsArgs = z.infer<
  z.ZodObject<typeof getLocalSerpResultsInputSchema>
>;
type GetGoogleBusinessQuestionsArgs = z.infer<
  z.ZodObject<typeof getGoogleBusinessQuestionsInputSchema>
>;

const QUESTIONS_ANSWERS_MIN_RADIUS = 200;
const QUESTIONS_ANSWERS_MAX_RADIUS = 199999;

function resolveMarketLocationCode(_market: Market | undefined): number {
  // The Zod enum on market.country already restricts values to United States
  // variants, so no other country can reach this code path.
  return DEFAULT_LOCATION_CODE;
}

function formatCoordinate(value: number): string {
  return Number(value.toFixed(7)).toString();
}

function formatBusinessLocationCoordinate(near: z.infer<typeof nearSchema>) {
  return `${formatCoordinate(near.latitude)},${formatCoordinate(near.longitude)},${near.radiusKm}`;
}

function formatQuestionsAnswersCoordinate(near: z.infer<typeof nearSchema>) {
  const radius = Math.min(
    QUESTIONS_ANSWERS_MAX_RADIUS,
    Math.max(QUESTIONS_ANSWERS_MIN_RADIUS, Math.round(near.radiusKm * 1000)),
  );
  return `${formatCoordinate(near.latitude)},${formatCoordinate(near.longitude)},${radius}`;
}

function formatLocalSerpCoordinate(near: z.infer<typeof localSerpNearSchema>) {
  const coordinate = `${formatCoordinate(near.latitude)},${formatCoordinate(near.longitude)}`;
  return near.zoom == null ? coordinate : `${coordinate},${near.zoom}z`;
}

function sortOrderByRankedMode(
  sortBy: GetRankedKeywordsArgs["sortBy"] = "search_volume",
): string[] {
  switch (sortBy) {
    case "rank":
      return ["ranked_serp_element.serp_item.rank_absolute,asc"];
    case "traffic_estimate":
      return ["ranked_serp_element.serp_item.etv,desc"];
    case "cpc":
      return ["keyword_data.keyword_info.cpc,desc"];
    case "search_volume":
      return ["keyword_data.keyword_info.search_volume,desc"];
  }
}

function pushAnd(filters: unknown[], condition: unknown[]) {
  if (filters.length > 0) filters.push("and");
  filters.push(condition);
}

function buildRankedKeywordFilters(args: {
  minSearchVolume?: number;
  maxRank?: number;
  excludeBrandTerms?: string[];
}) {
  const filters: unknown[] = [];
  if (args.minSearchVolume != null) {
    pushAnd(filters, [
      "keyword_data.keyword_info.search_volume",
      ">=",
      args.minSearchVolume,
    ]);
  }
  if (args.maxRank != null) {
    pushAnd(filters, [
      "ranked_serp_element.serp_item.rank_absolute",
      "<=",
      args.maxRank,
    ]);
  }
  if (args.excludeBrandTerms != null) {
    for (const term of args.excludeBrandTerms) {
      pushAnd(filters, ["keyword_data.keyword", "not_ilike", `%${term}%`]);
    }
  }
  return filters.length > 0 ? filters : undefined;
}

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function asRecord(value: unknown): Record<string, unknown> | undefined {
  return isRecord(value) ? value : undefined;
}

function displayValue(value: unknown): string {
  if (typeof value === "string" || typeof value === "number") {
    return String(value);
  }
  return "?";
}

function summarizeRankedKeyword(item: Record<string, unknown>): string {
  const keywordData = asRecord(item.keyword_data);
  const keywordInfo = asRecord(keywordData?.keyword_info);
  const serpElement = asRecord(item.ranked_serp_element);
  const serpItem = asRecord(serpElement?.serp_item);
  const keyword = displayValue(keywordData?.keyword ?? item.keyword);
  const rank = displayValue(
    serpItem?.rank_absolute ?? serpElement?.rank_absolute ?? item.rank_absolute,
  );
  const volume = displayValue(keywordInfo?.search_volume);
  const url = displayValue(serpItem?.url ?? serpElement?.url);
  return `- "${keyword}" #${rank} vol:${volume} ${url}`.trim();
}

function sortCompetitors(
  items: Record<string, unknown>[],
  sortBy: FindSerpCompetitorsArgs["sortBy"],
) {
  const field =
    sortBy === "avg_position"
      ? "avg_position"
      : sortBy === "keyword_count"
        ? "keywords_count"
        : sortBy === "traffic_estimate"
          ? "etv"
          : "visibility";
  const direction = sortBy === "avg_position" ? 1 : -1;
  return items.toSorted((a, b) => {
    const aValue = typeof a[field] === "number" ? a[field] : 0;
    const bValue = typeof b[field] === "number" ? b[field] : 0;
    return (aValue - bValue) * direction;
  });
}

function normalizeKeywordOverview(item: KeywordOverviewItem) {
  const info = item.keyword_info;
  // Only present when the caller opted into clickstream-refined volumes.
  const clickstreamInfo = item.keyword_info_normalized_with_clickstream;
  return {
    keyword: item.keyword,
    search_volume:
      clickstreamInfo?.search_volume ?? info?.search_volume ?? null,
    keyword_difficulty: item.keyword_properties?.keyword_difficulty ?? null,
    main_intent: item.search_intent_info?.main_intent ?? null,
    cpc: info?.cpc ?? null,
    competition: info?.competition ?? null,
    competition_level: info?.competition_level ?? null,
    monthly_searches:
      (clickstreamInfo?.search_volume
        ? clickstreamInfo.monthly_searches
        : info?.monthly_searches) ?? null,
  };
}

type KeywordMetricRow = ReturnType<typeof normalizeKeywordOverview>;

// Google Ads items (countries Labs doesn't cover) have no difficulty/intent.
function normalizeAdsKeyword(item: AdsKeywordItem): KeywordMetricRow {
  return {
    keyword: item.keyword,
    search_volume: item.search_volume ?? null,
    keyword_difficulty: null,
    main_intent: null,
    cpc: item.cpc ?? null,
    competition:
      item.competition_index != null ? item.competition_index / 100 : null,
    competition_level: item.competition ?? null,
    monthly_searches: item.monthly_searches ?? null,
  };
}

function sortKeywordMetricRows(
  rows: KeywordMetricRow[],
  sortBy: NonNullable<GetKeywordMetricsArgs["sortBy"]> = "search_volume",
) {
  return rows.toSorted((a, b) => {
    const aValue = a[sortBy];
    const bValue = b[sortBy];
    const aNum = typeof aValue === "number" ? aValue : 0;
    const bNum = typeof bValue === "number" ? bValue : 0;
    return bNum - aNum;
  });
}

function hostMatchesDomain(host: string, domain: string): boolean {
  const normalizedHost = host.replace(/^www\./, "").toLowerCase();
  const normalizedDomain = domain.replace(/^www\./, "").toLowerCase();
  return (
    normalizedHost === normalizedDomain ||
    normalizedHost.endsWith(`.${normalizedDomain}`)
  );
}

export const getRankedKeywordsTool = {
  name: "get_ranked_keywords",
  config: {
    title: "Get ranked keywords",
    description:
      "Returns exact keyword, URL, rank, search volume, CPC, intent, and traffic rows for a domain or page. Use this for strategy evidence; use get_domain_overview for aggregate domain footprint. Charges credits.",
    inputSchema: getRankedKeywordsInputSchema,
    outputSchema: {
      keywords: z.array(looseObjectOutputSchema),
      totalCount: z.number().nullable(),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(async (args: GetRankedKeywordsArgs, context) => {
    const client = createDataforseoClient(context.billing);
    const targetIsPage = /^https?:\/\//.test(args.target);
    const keywords = await client.domain.rankedKeywords({
      target: args.target,
      locationCode: resolveMarketLocationCode(args.market),
      languageCode: DEFAULT_LANGUAGE_CODE,
      limit: args.limit ?? 50,
      offset: args.offset,
      orderBy: sortOrderByRankedMode(args.sortBy),
      filters: buildRankedKeywordFilters({
        minSearchVolume: args.minSearchVolume,
        maxRank: args.maxRank,
        excludeBrandTerms: args.excludeBrandTerms,
      }),
      itemTypes: args.resultTypes,
      includeSubdomains: args.includeSubdomains ?? !targetIsPage,
    });

    return mcpResponse({
      text: [
        `Found ${keywords.items.length} ranked keyword rows for ${args.target}.`,
        ...keywords.items
          .slice(0, 10)
          .map((item) =>
            summarizeRankedKeyword(item as Record<string, unknown>),
          ),
      ].join("\n"),
      meta: buildProjectMeta(
        context,
        args.projectId,
        `/p/${args.projectId}/domain`,
      ),
      structuredContent: {
        keywords: keywords.items,
        totalCount: keywords.totalCount,
      },
    });
  }),
};

export const searchLocalBusinessesTool = {
  name: "search_local_businesses",
  config: {
    title: "Search local businesses",
    description:
      "Searches local business listings near a coordinate. Use this to find local business candidates or nearby competitors; it does not run Maps rank checks or Q&A. Charges credits.",
    inputSchema: searchLocalBusinessesInputSchema,
    outputSchema: {
      businesses: z.array(looseObjectOutputSchema),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(
    async (args: SearchLocalBusinessesArgs, context) => {
      const client = createDataforseoClient(context.billing);
      const businesses = await client.business.businessListings({
        categories: args.categories,
        title: args.query,
        locationCoordinate: formatBusinessLocationCoordinate(args.near),
        limit: args.limit ?? 20,
      });

      return mcpResponse({
        text: `Found ${businesses.length} local business rows${args.query ? ` for ${args.query}` : ""}.`,
        meta: buildProjectMeta(context, args.projectId, `/p/${args.projectId}`),
        structuredContent: { businesses },
      });
    },
  ),
};

export const getLocalSerpResultsTool = {
  name: "get_local_serp_results",
  config: {
    title: "Get local SERP results",
    description:
      "Fetches one Google Maps or Local Finder SERP near a coordinate. Returns provider rows with rank fields intact; callers decide how to match a target business. Charges credits.",
    inputSchema: getLocalSerpResultsInputSchema,
    outputSchema: {
      results: z.array(looseObjectOutputSchema),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(
    async (args: GetLocalSerpResultsArgs, context) => {
      const client = createDataforseoClient(context.billing);
      const results = await client.serp.local({
        keyword: args.keyword,
        locationCoordinate: formatLocalSerpCoordinate(args.near),
        languageCode: args.languageCode ?? DEFAULT_LANGUAGE_CODE,
        searchType: args.searchType ?? "maps",
        device: args.device ?? "desktop",
        depth: args.depth ?? 20,
        searchPlaces: false,
      });

      return mcpResponse({
        text: `Fetched ${results.length} local SERP rows for "${args.keyword}".`,
        meta: buildProjectMeta(context, args.projectId, `/p/${args.projectId}`),
        structuredContent: { results },
      });
    },
  ),
};

export const getGoogleBusinessQuestionsTool = {
  name: "get_google_business_questions",
  config: {
    title: "Get Google business questions",
    description:
      "Fetches Google Business Profile questions and answers for one business keyword near a coordinate. Run this only when Q&A evidence is needed. Charges credits.",
    inputSchema: getGoogleBusinessQuestionsInputSchema,
    outputSchema: {
      questions: z.array(looseObjectOutputSchema),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(
    async (args: GetGoogleBusinessQuestionsArgs, context) => {
      const client = createDataforseoClient(context.billing);
      const questions = await client.business.questionsAnswers({
        keyword: args.keyword,
        locationCoordinate: formatQuestionsAnswersCoordinate(args.near),
        languageCode: args.languageCode ?? DEFAULT_LANGUAGE_CODE,
        depth: args.depth ?? 20,
      });

      return mcpResponse({
        text: `Fetched ${questions.length} Google Business Q&A rows for ${args.keyword}.`,
        meta: buildProjectMeta(context, args.projectId, `/p/${args.projectId}`),
        structuredContent: { questions },
      });
    },
  ),
};

export const findSerpCompetitorsTool = {
  name: "find_serp_competitors",
  config: {
    title: "Find SERP competitors",
    description:
      "Compares domains competing in Google results for a supplied keyword set. Useful for market and search-intelligence reports; not radius-based local SEO. Charges credits.",
    inputSchema: findSerpCompetitorsInputSchema,
    outputSchema: {
      competitors: z.array(looseObjectOutputSchema),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(
    async (args: FindSerpCompetitorsArgs, context) => {
      const client = createDataforseoClient(context.billing);
      const competitors = await client.labs.serpCompetitors({
        keywords: args.keywords,
        locationCode: resolveMarketLocationCode(args.market),
        languageCode: DEFAULT_LANGUAGE_CODE,
        itemTypes: args.resultTypes ?? ["organic", "local_pack"],
        includeSubdomains: args.includeSubdomains,
        limit: args.limit ?? 50,
        offset: args.offset,
      });
      const excludedDomains = args.excludeDomains ?? [];
      const filtered =
        excludedDomains.length === 0
          ? competitors
          : competitors.filter((item) => {
              const domain = typeof item.domain === "string" ? item.domain : "";
              return !excludedDomains.some((excludedDomain) =>
                hostMatchesDomain(domain, excludedDomain),
              );
            });
      const sorted = sortCompetitors(filtered, args.sortBy ?? "visibility");

      return mcpResponse({
        text: `Found ${sorted.length} SERP competitors across ${args.keywords.length} keywords.`,
        meta: buildProjectMeta(
          context,
          args.projectId,
          `/p/${args.projectId}/domain`,
        ),
        structuredContent: { competitors: sorted },
      });
    },
  ),
};

export const getKeywordMetricsTool = {
  name: "get_keyword_metrics",
  config: {
    title: "Get keyword metrics",
    description:
      "Hydrate up to 700 known keywords with search volume, keyword difficulty (KD), search intent, CPC, competition, and monthly trends in a single call. Use it to score candidate or known keywords — including Search Console striking-distance queries — by real demand and ranking difficulty. For countries served from Google Ads data (e.g. Iceland), KD and intent are null. Charges credits.",
    inputSchema: getKeywordMetricsInputSchema,
    outputSchema: {
      keywords: z.array(looseObjectOutputSchema),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: false,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(async (args: GetKeywordMetricsArgs, context) => {
    const client = createDataforseoClient(context.billing);
    const locationCode = args.locationCode ?? DEFAULT_LOCATION_CODE;
    const languageCode = args.languageCode ?? DEFAULT_LANGUAGE_CODE;
    const normalized =
      getKeywordDataProvider(locationCode) === "google_ads"
        ? (
            await client.keywords.adsSearchVolume({
              keywords: args.keywords,
              locationCode,
              languageCode,
              creditFeature: "keyword_research",
            })
          ).map(normalizeAdsKeyword)
        : (
            await client.labs.keywordOverview({
              keywords: args.keywords,
              locationCode,
              languageCode,
              includeClickstreamData: args.includeClickstreamData ?? false,
              creditFeature: "keyword_research",
            })
          ).map(normalizeKeywordOverview);
    const rows = sortKeywordMetricRows(
      normalized,
      args.sortBy ?? "search_volume",
    ).map((row) =>
      args.includeMonthlyTrends === false
        ? Object.fromEntries(
            Object.entries(row).filter(([key]) => key !== "monthly_searches"),
          )
        : row,
    );

    return mcpResponse({
      text: `Fetched metrics (volume, difficulty, intent) for ${rows.length} keywords.`,
      meta: buildProjectMeta(
        context,
        args.projectId,
        `/p/${args.projectId}/keywords`,
      ),
      structuredContent: { keywords: rows },
    });
  }),
};
