/* eslint-disable max-lines */
import { z } from "zod";
import { buildProjectMeta } from "@/server/mcp/context";
import { mcpResponse } from "@/server/mcp/formatters";
import { optionalMetaOutputSchema } from "@/server/mcp/output-schemas";
import { withMcpProjectAuth } from "@/server/mcp/project-auth";
import { projectIdSchema } from "@/server/mcp/schemas";
import { buildDashboardUrl } from "@/server/mcp/urls";
import { hasSelfHostedGscConfig } from "@/server/features/gsc/oauth-config";
import { isHostedServerAuthMode } from "@/server/lib/runtime-env";
import {
  GscNotConnectedError,
  GscService,
} from "@/server/features/gsc/services/GscService";
import {
  GSC_DATE_RANGES,
  GSC_DEFAULT_ROW_LIMIT,
  GSC_DIMENSIONS,
  GSC_FILTER_OPERATORS,
  GSC_MAX_ROW_LIMIT,
  GSC_SEARCH_TYPES,
  type GscPerformanceInput,
} from "@/server/features/gsc/searchAnalytics";
import { GscApiError, GscTokenError } from "@/server/lib/gscClient";
import { GSC_SELF_HOSTED_SETUP_DOCS_URL } from "@/shared/gsc";

const TEXT_SUMMARY_ROWS = 15;

type ProjectAuthContext = {
  auth: { organizationId: string };
  baseUrl: string;
};

function connectGscUrl(baseUrl: string, projectId: string): string {
  return buildDashboardUrl(baseUrl, `/p/${projectId}/settings#search-console`);
}

/** Self-hosted GSC requires the operator to provide a Google OAuth client and
 *  BETTER_AUTH_SECRET. Hosted mode always has both; self-hosted tools return this
 *  setup nudge before attempting a token lookup when either is missing. */
async function missingSelfHostedGoogleClientResponse(
  context: ProjectAuthContext,
  projectId: string,
) {
  const [hosted, configured] = await Promise.all([
    isHostedServerAuthMode(),
    hasSelfHostedGscConfig(),
  ]);
  if (hosted || configured) return null;

  return mcpResponse({
    text: `This self-hosted OpenSEO deployment is not configured for Search Console yet. Set GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and BETTER_AUTH_SECRET, then reconnect Search Console from the project's settings page. Setup docs: ${GSC_SELF_HOSTED_SETUP_DOCS_URL}`,
    meta: buildProjectMeta(context, projectId),
    structuredContent: {
      ok: false,
      connected: false,
      reason: "gsc_oauth_not_configured",
      setupDocsUrl: GSC_SELF_HOSTED_SETUP_DOCS_URL,
    },
  });
}

function invalidRequest(
  meta: ReturnType<typeof buildProjectMeta>,
  message: string,
) {
  return mcpResponse({
    text: message,
    meta,
    structuredContent: { ok: false, reason: "invalid_request" },
  });
}

function describeGscError(error: unknown): string {
  if (error instanceof GscNotConnectedError) {
    return "Search Console is not connected for this project.";
  }
  if (error instanceof GscTokenError) {
    return "The Search Console connection has expired or was revoked. Reconnect it to continue.";
  }
  if (error instanceof GscApiError) {
    return error.message;
  }
  return error instanceof Error ? error.message : String(error);
}

// ---------------------------------------------------------------------------
// get_search_console_performance
// ---------------------------------------------------------------------------

const filterSchema = z.object({
  dimension: z.enum(GSC_DIMENSIONS),
  operator: z.enum(GSC_FILTER_OPERATORS).default("equals"),
  expression: z.string().min(1),
});

const perfInputSchema = {
  projectId: projectIdSchema,
  dimensions: z
    .array(z.enum(GSC_DIMENSIONS))
    .min(1)
    .max(4)
    .optional()
    .describe(
      "Group rows by these dimensions. Default ['query']. Use ['page'] for top pages, ['query','page'] to map queries to pages / spot cannibalization, ['date'] for a time series.",
    ),
  dateRange: z
    .enum(GSC_DATE_RANGES)
    .optional()
    .describe(
      "Convenience window (default last_28_days). End is set ~3 days back for GSC data lag. Ignored if startDate+endDate are given. Max lookback is 16 months.",
    ),
  startDate: z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/)
    .optional()
    .describe("Explicit start (YYYY-MM-DD, Pacific Time). Use with endDate."),
  endDate: z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/)
    .optional()
    .describe("Explicit end (YYYY-MM-DD, Pacific Time). Use with startDate."),
  filters: z
    .array(filterSchema)
    .max(5)
    .optional()
    .describe(
      "AND-combined filters. To get the queries for one page: [{dimension:'page',operator:'equals',expression:'https://example.com/post'}] with dimensions ['query'].",
    ),
  rowLimit: z
    .number()
    .int()
    .min(1)
    .max(GSC_MAX_ROW_LIMIT)
    .optional()
    .describe(
      "Rows per call (default 1000, max 1000). GSC sorts by clicks desc and can't filter by position — filter 'striking distance' positions client-side, and paginate with startRow when hasMore is true.",
    ),
  startRow: z.number().int().min(0).optional().describe("Pagination offset."),
  type: z
    .enum(GSC_SEARCH_TYPES)
    .optional()
    .describe("Search type (default web)."),
  dataState: z
    .enum(["all", "final"])
    .optional()
    .describe("'all' (default) includes fresh/incomplete recent data."),
} as const;

type PerfArgs = z.infer<z.ZodObject<typeof perfInputSchema>>;

export const getSearchConsolePerformanceTool = {
  name: "get_search_console_performance",
  config: {
    title: "Get Google Search Console performance",
    description:
      "Query the connected Search Console property's Search Analytics: clicks, impressions, CTR, and average position by query/page/country/device/date. First-party data — use it for what already ranks, near-ranking queries, and pages with real demand. ctr is a 0-1 fraction; position is a 1-based average; dates are Pacific Time; the last ~3 days may be incomplete. Read-only; uses no credits.",
    inputSchema: perfInputSchema,
    outputSchema: {
      ok: z.boolean(),
      reason: z.string().optional(),
      connectUrl: z.string().optional(),
      setupDocsUrl: z.string().optional(),
      siteUrl: z.string().optional(),
      startDate: z.string().optional(),
      endDate: z.string().optional(),
      dimensions: z.array(z.string()).optional(),
      rowCount: z.number().optional(),
      rows: z
        .array(
          z
            .object({
              keys: z.array(z.string()).optional(),
              clicks: z.number(),
              impressions: z.number(),
              ctr: z.number(),
              position: z.number(),
            })
            .passthrough(),
        )
        .optional(),
      hasMore: z.boolean().optional(),
      nextStartRow: z.number().optional(),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: true,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(async (args: PerfArgs, context) => {
    const blocked = await missingSelfHostedGoogleClientResponse(
      context,
      args.projectId,
    );
    if (blocked) return blocked;

    const connectUrl = connectGscUrl(context.baseUrl, args.projectId);
    const meta = buildProjectMeta(
      context,
      args.projectId,
      `/p/${args.projectId}/settings`,
    );

    // GSC rejects searchAppearance combined with any other dimension.
    if (
      args.dimensions &&
      args.dimensions.includes("searchAppearance") &&
      args.dimensions.length > 1
    ) {
      return invalidRequest(
        meta,
        "searchAppearance must be the only dimension when used.",
      );
    }
    // A half-specified explicit range would silently fall back to a default window.
    if (Boolean(args.startDate) !== Boolean(args.endDate)) {
      return invalidRequest(
        meta,
        "Provide both startDate and endDate, or neither (use dateRange instead).",
      );
    }

    try {
      const result = await GscService.getPerformance(
        args satisfies GscPerformanceInput,
      );
      const dimensions = result.request.dimensions ?? ["query"];
      // rowLimit is clamped to the agent cap in buildSearchAnalyticsRequest, so
      // result.rows is already <= the cap — every count below reflects what we return.
      const rows = result.rows;
      const requestedLimit = result.request.rowLimit ?? GSC_DEFAULT_ROW_LIMIT;
      const hasMore = rows.length >= requestedLimit;
      const nextStartRow = (result.request.startRow ?? 0) + rows.length;

      const summaryLines = rows.slice(0, TEXT_SUMMARY_ROWS).map((r) => {
        const label = r.keys?.join(" / ") ?? "(total)";
        const ctrPct = (r.ctr * 100).toFixed(1);
        return `  ${label} — ${r.clicks} clicks, ${r.impressions} impr, ${ctrPct}% CTR, pos ${r.position.toFixed(1)}`;
      });
      const header =
        `${result.siteUrl} · ${dimensions.join("+")} · ${result.request.startDate}→${result.request.endDate} · ` +
        `${rows.length} row${rows.length === 1 ? "" : "s"}${hasMore ? " (more available — paginate with startRow)" : ""}`;
      const text =
        summaryLines.length > 0
          ? `${header}\n${summaryLines.join("\n")}${rows.length > summaryLines.length ? `\n  …and ${rows.length - summaryLines.length} more` : ""}`
          : `${header}\nNo rows for this query/date range.`;

      return mcpResponse({
        text,
        meta,
        structuredContent: {
          ok: true,
          siteUrl: result.siteUrl,
          startDate: result.request.startDate,
          endDate: result.request.endDate,
          dimensions,
          rowCount: rows.length,
          rows,
          hasMore,
          nextStartRow: hasMore ? nextStartRow : undefined,
        },
      });
    } catch (error) {
      const isNotConnected = error instanceof GscNotConnectedError;
      return mcpResponse({
        text: `${describeGscError(error)}${isNotConnected ? ` Connect it here: ${connectUrl}` : ` (reconnect at ${connectUrl})`}`,
        meta,
        structuredContent: {
          ok: false,
          reason: isNotConnected ? "not_connected" : "api_error",
          connectUrl,
        },
      });
    }
  }),
};

// ---------------------------------------------------------------------------
// inspect_urls
// ---------------------------------------------------------------------------

const inspectInputSchema = {
  projectId: projectIdSchema,
  urls: z
    .array(z.string().url())
    .min(1)
    .max(10)
    .describe(
      "1–10 absolute URLs to inspect. Each must belong to the connected property.",
    ),
  languageCode: z
    .string()
    .optional()
    .describe("BCP-47 language for the inspection result (e.g. 'en-US')."),
} as const;

type InspectArgs = z.infer<z.ZodObject<typeof inspectInputSchema>>;

export const inspectUrlsTool = {
  name: "inspect_urls",
  config: {
    title: "Inspect URLs in Google Search Console",
    description:
      "Run Google Search Console's URL Inspection on up to 10 URLs of the connected property: index/coverage state, last crawl time, Google-selected vs declared canonical, and mobile/rich-results verdicts. Use it to answer 'is this page indexed? why not?'. Per-URL failures are reported inline. Read-only; uses no credits.",
    inputSchema: inspectInputSchema,
    outputSchema: {
      ok: z.boolean(),
      reason: z.string().optional(),
      connectUrl: z.string().optional(),
      setupDocsUrl: z.string().optional(),
      siteUrl: z.string().optional(),
      results: z
        .array(
          z
            .object({
              url: z.string(),
              result: z.unknown().nullable().optional(),
              error: z.string().optional(),
            })
            .passthrough(),
        )
        .optional(),
      ...optionalMetaOutputSchema,
    },
    annotations: {
      readOnlyHint: true,
      openWorldHint: true,
      destructiveHint: false,
    },
  },
  handler: withMcpProjectAuth(async (args: InspectArgs, context) => {
    const blocked = await missingSelfHostedGoogleClientResponse(
      context,
      args.projectId,
    );
    if (blocked) return blocked;

    const connectUrl = connectGscUrl(context.baseUrl, args.projectId);
    const meta = buildProjectMeta(
      context,
      args.projectId,
      `/p/${args.projectId}/settings`,
    );

    try {
      const { siteUrl, results } = await GscService.inspectUrls({
        projectId: args.projectId,
        urls: args.urls,
        languageCode: args.languageCode,
      });

      const summaryLines = results.slice(0, TEXT_SUMMARY_ROWS).map((r) => {
        if (r.error) return `  ${r.url} — error: ${r.error}`;
        const index = r.result?.indexStatusResult;
        const verdict = index?.verdict ?? "UNKNOWN";
        const coverage = index?.coverageState ?? "—";
        const canonical = index?.googleCanonical
          ? `, google-canonical ${index.googleCanonical}`
          : "";
        return `  ${r.url} — ${verdict}: ${coverage}${canonical}`;
      });
      const text =
        `${siteUrl} · inspected ${results.length} URL${results.length === 1 ? "" : "s"}\n` +
        summaryLines.join("\n");

      return mcpResponse({
        text,
        meta,
        structuredContent: { ok: true, siteUrl, results },
      });
    } catch (error) {
      const isNotConnected = error instanceof GscNotConnectedError;
      return mcpResponse({
        text: `${describeGscError(error)}${isNotConnected ? ` Connect it here: ${connectUrl}` : ` (reconnect at ${connectUrl})`}`,
        meta,
        structuredContent: {
          ok: false,
          reason: isNotConnected ? "not_connected" : "api_error",
          connectUrl,
        },
      });
    }
  }),
};
