import type { Tool, ToolExecutionContext, ToolResult } from '../types.js';

/** Configuration for the web search tool. */
export interface WebSearchConfig {
  provider: 'brave' | 'searxng';
  /** Required for Brave Search API. */
  apiKey?: string;
  /** Required for SearXNG (e.g. 'http://searxng:8080'). */
  endpoint?: string;
  /** Optional fallback SearXNG endpoint (for example a homelab instance). */
  fallbackEndpoint?: string;
  /** Maximum number of results to return (default: 5). */
  maxResults?: number;
}

interface WebSearchArgs {
  query: string;
  count?: number;
  country?: string;
  searchLang?: string;
  safeSearch?: 'off' | 'moderate' | 'strict';
  freshness?: 'pd' | 'pw' | 'pm' | 'py';
}

interface SearchResult {
  title: string;
  url: string;
  snippet: string;
  age?: string;
}

/** Fetch timeout in milliseconds. */
const FETCH_TIMEOUT_MS = 10_000;

/** Maximum allowed result count. */
const MAX_RESULTS = 20;

/** Default result count. */
const DEFAULT_RESULTS = 5;

/**
 * Format search results as numbered markdown.
 */
function formatResults(results: SearchResult[]): string {
  return results
    .map(
      (r, i) => `${i + 1}. **${r.title}** \u2014 ${r.url}${r.age ? ` (${r.age})` : ''}\n   ${r.snippet}`,
    )
    .join('\n\n');
}

function resolveCount(requestedCount: number | undefined, defaultCount: number): number {
  const normalized = Number.isFinite(requestedCount)
    ? Math.trunc(requestedCount as number)
    : defaultCount;
  return Math.min(Math.max(normalized, 1), MAX_RESULTS);
}

function createBraveHeaders(apiKey: string): Record<string, string> {
  return {
    'Accept': 'application/json',
    'X-Subscription-Token': apiKey,
  };
}

function createFetchSignal(signal?: AbortSignal): AbortSignal {
  const timeoutSignal = AbortSignal.timeout(FETCH_TIMEOUT_MS);
  return signal
    ? AbortSignal.any([signal, timeoutSignal])
    : timeoutSignal;
}

function appendBraveCommonQueryParams(params: URLSearchParams, args: WebSearchArgs): void {
  params.set('q', args.query);
  params.set('count', String(args.count));

  if (args.country) {
    params.set('country', args.country);
  }
  if (args.searchLang) {
    params.set('search_lang', args.searchLang);
  }
  if (args.freshness) {
    params.set('freshness', args.freshness);
  }
}

async function fetchBraveEndpoint<TResponse>(
  endpoint: 'web' | 'news',
  args: WebSearchArgs,
  apiKey: string,
  signal?: AbortSignal,
): Promise<TResponse> {
  const params = new URLSearchParams();
  appendBraveCommonQueryParams(params, args);
  if (endpoint === 'web' && args.safeSearch) {
    params.set('safesearch', args.safeSearch);
  }

  const response = await fetch(
    `https://api.search.brave.com/res/v1/${endpoint}/search?${params.toString()}`,
    {
      signal: createFetchSignal(signal),
      headers: createBraveHeaders(apiKey),
    },
  );

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Brave API HTTP ${response.status}: ${body}`);
  }

  return (await response.json()) as TResponse;
}

/**
 * Search using the Brave Search API.
 */
async function searchBraveWeb(
  args: WebSearchArgs,
  apiKey: string,
  signal?: AbortSignal,
): Promise<SearchResult[]> {
  const data = await fetchBraveEndpoint<{
    web?: { results?: Array<{ title: string; url: string; description: string }> };
  }>('web', args, apiKey, signal);

  const rawResults = data.web?.results ?? [];
  return rawResults.map((r) => ({
    title: r.title,
    url: r.url,
    snippet: r.description ?? '',
  }));
}

async function searchBraveNews(
  args: WebSearchArgs,
  apiKey: string,
  signal?: AbortSignal,
): Promise<SearchResult[]> {
  const data = await fetchBraveEndpoint<{
    results?: Array<{ title: string; url: string; description?: string; age?: string }>;
    news?: { results?: Array<{ title: string; url: string; description?: string; age?: string }> };
  }>('news', args, apiKey, signal);

  const rawResults = data.results ?? data.news?.results ?? [];
  return rawResults.map((r) => ({
    title: r.title,
    url: r.url,
    snippet: r.description ?? '',
    age: r.age,
  }));
}

/**
 * Search using a self-hosted SearXNG instance.
 */
async function searchSearxng(
  query: string,
  count: number,
  endpoint: string,
  signal?: AbortSignal,
): Promise<SearchResult[]> {
  // Strip trailing slash from endpoint
  const baseUrl = endpoint.replace(/\/+$/, '');
  const params = new URLSearchParams({
    q: query,
    format: 'json',
    categories: 'general',
  });

  const response = await fetch(`${baseUrl}/search?${params.toString()}`, {
    signal: createFetchSignal(signal),
    headers: {
      'Accept': 'application/json',
    },
  });

  if (!response.ok) {
    const body = await response.text();
    throw new Error(`SearXNG HTTP ${response.status}: ${body}`);
  }

  const data = (await response.json()) as {
    results?: Array<{ title: string; url: string; content: string }>;
  };

  const rawResults = (data.results ?? []).slice(0, count);
  return rawResults.map((r) => ({
    title: r.title,
    url: r.url,
    snippet: r.content ?? '',
  }));
}

function normalizeEndpoint(endpoint: string | undefined): string | null {
  if (typeof endpoint !== 'string') {
    return null;
  }
  const trimmed = endpoint.trim();
  return trimmed.length > 0 ? trimmed : null;
}

function resolveSearxngEndpoints(config: WebSearchConfig): string[] {
  const endpoints: string[] = [];
  const primary = normalizeEndpoint(config.endpoint);
  const fallback = normalizeEndpoint(config.fallbackEndpoint);

  if (primary) {
    endpoints.push(primary);
  }
  if (fallback && fallback !== primary) {
    endpoints.push(fallback);
  }

  return endpoints;
}

function normalizeErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

async function searchSearxngWithFallback(
  query: string,
  count: number,
  config: WebSearchConfig,
  signal?: AbortSignal,
): Promise<SearchResult[]> {
  const endpoints = resolveSearxngEndpoints(config);
  if (endpoints.length === 0) {
    throw new Error('SearXNG endpoint not configured');
  }

  if (endpoints.length === 1) {
    return searchSearxng(query, count, endpoints[0] as string, signal);
  }

  const [primary, fallback] = endpoints;
  try {
    return await searchSearxng(query, count, primary as string, signal);
  } catch (primaryError) {
    if (signal?.aborted) {
      throw primaryError;
    }

    try {
      return await searchSearxng(query, count, fallback as string, signal);
    } catch (fallbackError) {
      throw new Error(
        `SearXNG primary failed: ${normalizeErrorMessage(primaryError)}; fallback failed: ${normalizeErrorMessage(fallbackError)}`,
      );
    }
  }
}

function normalizeToolError(error: unknown): ToolResult {
  if (error instanceof Error && (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'))) {
    return { success: false, output: '', error: 'Operation aborted' };
  }
  return {
    success: false,
    output: '',
    error: error instanceof Error ? error.message : String(error),
  };
}

function noResults(query: string): ToolResult {
  return {
    success: true,
    output: `No results found for: ${query}`,
  };
}

/**
 * Creates a web.search tool configured for the given search provider.
 *
 * Supports Brave Search API and self-hosted SearXNG.
 */
export function createWebSearchTool(config: WebSearchConfig): Tool {
  const defaultCount = config.maxResults ?? DEFAULT_RESULTS;

  return {
    name: 'web.search',
    description:
      'Search the web for current information. Returns titles, URLs, and snippets from web search results. Use this to find up-to-date information about any topic.',
    requiredSecretScopes: ['web_search'],
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'Search query' },
        count: {
          type: 'number',
          description: 'Number of results to return (default 5, max 20)',
        },
        country: {
          type: 'string',
          description: 'Optional country code for regionalized results (for example: us, gb, de)',
        },
        searchLang: {
          type: 'string',
          description: 'Optional language code for search results (for example: en, fr, de)',
        },
        safeSearch: {
          type: 'string',
          enum: ['off', 'moderate', 'strict'],
          description: 'Brave-only safesearch level',
        },
        freshness: {
          type: 'string',
          enum: ['pd', 'pw', 'pm', 'py'],
          description: 'Brave freshness filter: past day/week/month/year',
        },
      },
      required: ['query'],
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as WebSearchArgs;
      if (context?.signal?.aborted) {
        return { success: false, output: '', error: 'Operation aborted' };
      }
      // Clamp count: use provided value (capped at MAX_RESULTS), or fall back to default
      const count = resolveCount(args.count, defaultCount);

      try {
        let results: SearchResult[];
        const braveArgs: WebSearchArgs = { ...args, count };

        if (config.provider === 'brave') {
          if (!config.apiKey) {
            return {
              success: false,
              output: '',
              error: 'Brave Search API key not configured',
            };
          }
          results = await searchBraveWeb(braveArgs, config.apiKey, context?.signal);
        } else {
          // SearXNG provider
          results = await searchSearxngWithFallback(args.query, count, config, context?.signal);
        }

        if (results.length === 0) {
          return noResults(args.query);
        }

        return { success: true, output: formatResults(results) };
      } catch (error) {
        return normalizeToolError(error);
      }
    },
  };
}

/**
 * Brave Search API: dedicated news search endpoint.
 */
export function createBraveNewsSearchTool(config: WebSearchConfig): Tool {
  const defaultCount = config.maxResults ?? DEFAULT_RESULTS;

  return {
    name: 'web.search.news',
    description:
      'Search recent news via Brave Search and return titles, URLs, snippets, and age metadata when available.',
    requiredSecretScopes: ['web_search'],
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string', description: 'News search query' },
        count: {
          type: 'number',
          description: 'Number of results to return (default 5, max 20)',
        },
        country: {
          type: 'string',
          description: 'Optional country code for regionalized news results (for example: us, gb, de)',
        },
        searchLang: {
          type: 'string',
          description: 'Optional language code for news results (for example: en, fr, de)',
        },
        freshness: {
          type: 'string',
          enum: ['pd', 'pw', 'pm', 'py'],
          description: 'Freshness filter: past day/week/month/year',
        },
      },
      required: ['query'],
    },
    execute: async (rawArgs: unknown, context?: ToolExecutionContext): Promise<ToolResult> => {
      const args = rawArgs as WebSearchArgs;
      if (context?.signal?.aborted) {
        return { success: false, output: '', error: 'Operation aborted' };
      }
      if (!config.apiKey) {
        return {
          success: false,
          output: '',
          error: 'Brave Search API key not configured',
        };
      }

      try {
        const results = await searchBraveNews(
          { ...args, count: resolveCount(args.count, defaultCount) },
          config.apiKey,
          context?.signal,
        );

        if (results.length === 0) {
          return noResults(args.query);
        }

        return { success: true, output: formatResults(results) };
      } catch (error) {
        return normalizeToolError(error);
      }
    },
  };
}
