import * as z from "zod/v4";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Registry } from "../registry/index.js";
import type { HarnessClient } from "../client/harness-client.js";
import { jsonResult, errorResult } from "../utils/response-formatter.js";
import { isUserError, isUserFixableApiError, toMcpError } from "../utils/errors.js";
import { compactItems } from "../utils/compact.js";
import { createLogger } from "../utils/logger.js";
import { sendProgress, sendLog } from "../utils/progress.js";
import { applyUrlDefaults } from "../utils/url-parser.js";
import type { ResourceScope } from "../registry/types.js";
import { searchOutputSchema } from "./output-schemas.js";

const log = createLogger("search");
const RESOURCE_SCOPES: readonly ResourceScope[] = ["account", "org", "project"];

/** Relevance tiers — lower number = more relevant. */
const RELEVANCE_TIERS: Record<string, number> = {
  pipeline: 1, service: 1, environment: 1, connector: 1, execution: 1,
  template: 2, trigger: 2, input_set: 2, secret: 2, fme_feature_flag: 2,
  repository: 2, infrastructure: 2,
  // SCS types at tier 2 — same priority as other domain-specific resources
  scs_artifact_source: 2, artifact_security: 2, code_repo_security: 2,
  scs_artifact_component: 2, scs_compliance_result: 2,
  // Everything else defaults to tier 3
};

function getTier(resourceType: string): number {
  return RELEVANCE_TIERS[resourceType] ?? 3;
}

function asResourceScope(value: unknown): ResourceScope | undefined {
  return typeof value === "string" && RESOURCE_SCOPES.includes(value as ResourceScope)
    ? value as ResourceScope
    : undefined;
}

interface SearchResultEntry {
  resource_type: string;
  tier: number;
  match_count: number;
  items: unknown[];
  total: number;
  openInHarness?: string;
}

export function registerSearchTool(server: McpServer, registry: Registry, client: HarnessClient): void {
  const listableTypes = registry.getTypesForOperation("list") as [string, ...string[]];

  server.registerTool(
    "harness_search",
    {
      description: "Search across multiple Harness resource types. Returns results ranked by relevance. Accepts a Harness URL for scope.",
      inputSchema: {
        query: z.string().describe("Search term"),
        resource_types: z.array(z.enum(listableTypes)).describe("Types to search (defaults to all listable)").optional(),
        url: z.string().describe("Harness UI URL — auto-extracts org and project").optional(),
        resource_scope: z.enum(["account", "org", "project"]).optional().describe("Scope to search. Use account for account-level resources and to omit org/project defaults; org injects only org; project injects org+project. Auto-detected from url."),
        org_id: z.string().describe("Organization identifier (overrides default)").optional(),
        project_id: z.string().describe("Project identifier (overrides default)").optional(),
        max_per_type: z.number().describe("Max results per type").default(5).optional(),
        compact: z.boolean().describe("Strip verbose metadata (default true)").default(true).optional(),
      },
      outputSchema: searchOutputSchema,
      annotations: {
        title: "Search Harness Resources",
        readOnlyHint: true,
        destructiveHint: false,
        openWorldHint: true,
      },
    },
    async (args, extra) => {
      try {
        const signal = extra.signal;
        const mergedArgs = applyUrlDefaults(args as Record<string, unknown>, args.url, { includeResourceScope: true });
        const requestedScope = asResourceScope(mergedArgs.resource_scope);
        const hasExplicitResourceTypes = (args.resource_types?.length ?? 0) > 0;
        // Determine which resource types to search
        let targetTypes = args.resource_types ?? [];
        if (targetTypes.length === 0) {
          // Search all types that support list
          targetTypes = registry.getAllResourceTypes().filter((rt) => registry.supportsOperation(rt, "list"));
        }
        if (requestedScope && !hasExplicitResourceTypes) {
          targetTypes = targetTypes.filter((rt) => registry.getSupportedScopes(rt).includes(requestedScope));
        }

        const entries: SearchResultEntry[] = [];
        const errors: Record<string, string> = {};

        // Run searches with concurrency limit to avoid overwhelming the API
        const MAX_CONCURRENCY = 5;
        const settled: { rt: string; result: unknown; error: string | null }[] = [];
        let searched = 0;

        for (let i = 0; i < targetTypes.length; i += MAX_CONCURRENCY) {
          // Check for cancellation between batches
          signal.throwIfAborted();
          const batch = targetTypes.slice(i, i + MAX_CONCURRENCY);
          await sendProgress(extra, searched, targetTypes.length, `Searching batch ${Math.floor(i / MAX_CONCURRENCY) + 1}...`);
          const batchResults = await Promise.all(
            batch.map(async (rt) => {
              try {
                const result = await registry.dispatch(client, rt, "list", {
                  ...mergedArgs,
                  search_term: args.query,
                  name: args.query,
                  query: args.query,
                  search: args.query,
                  size: args.max_per_type ?? 5,
                  limit: args.max_per_type ?? 5,
                  page: 0,
                }, { tool: "harness_search" }, signal);
                return { rt, result, error: null };
              } catch (err) {
                log.debug(`Search failed for ${rt}`, { error: String(err) });
                return { rt, result: null, error: String(err) };
              }
            }),
          );
          settled.push(...batchResults);
          searched += batch.length;
        }
        await sendProgress(extra, targetTypes.length, targetTypes.length, "Processing results...");
        let totalMatches = 0;

        for (const { rt, result, error } of settled) {
          if (result) {
            const r = result as { items?: unknown[]; total?: number; openInHarness?: string };
            if (r.items && r.items.length > 0) {
              const items = args.compact !== false ? compactItems(r.items) : r.items;
              const matchCount = r.items.length;
              totalMatches += matchCount;
              entries.push({
                resource_type: rt,
                tier: getTier(rt),
                match_count: matchCount,
                items,
                total: r.total ?? matchCount,
                ...(r.openInHarness ? { openInHarness: r.openInHarness } : {}),
              });
            }
          }
          if (error) {
            errors[rt] = error;
          }
        }

        // Sort by tier ascending, then by match_count descending within tier
        entries.sort((a, b) => {
          if (a.tier !== b.tier) return a.tier - b.tier;
          return b.match_count - a.match_count;
        });

        return jsonResult({
          query: args.query,
          total_matches: totalMatches,
          searched_types: targetTypes.length,
          results: entries,
          ...(Object.keys(errors).length > 0 ? { errors } : {}),
        });
      } catch (err) {
        if (isUserError(err)) return errorResult(err.message);
        if (isUserFixableApiError(err)) return errorResult(err.message);
        throw toMcpError(err);
      }
    },
  );
}
