import type {
  SearchApiConfig,
  SearchProvider,
  SearchProviderConfigs,
  SearXngCategory,
  SerpApiEngine,
} from "@/stores/wiki-store"
import { getHttpFetch, isFetchNetworkError } from "@/lib/tauri-fetch"
import { hasConfiguredAnyTxt, normalizeAnyTxtConfig } from "@/lib/anytxt-search"

export interface WebSearchResult {
  title: string
  url: string
  snippet: string
  source: string
}

export const SERPAPI_ENGINE_OPTIONS: { value: SerpApiEngine; label: string; hint: string }[] = [
  { value: "google", label: "Google Web", hint: "SerpApi Google Search API organic results" },
  { value: "google_news", label: "Google News", hint: "News-focused results" },
  { value: "google_scholar", label: "Google Scholar", hint: "Academic papers and citations" },
  { value: "google_patents", label: "Google Patents", hint: "Patent search results" },
  { value: "bing", label: "Bing", hint: "Bing organic results" },
  { value: "duckduckgo", label: "DuckDuckGo", hint: "DuckDuckGo organic results" },
  { value: "google_images", label: "Google Images", hint: "Image search results" },
  { value: "google_videos", label: "Google Videos", hint: "Video search results" },
  { value: "youtube", label: "YouTube", hint: "YouTube video results" },
]

export const SEARXNG_CATEGORY_OPTIONS: { value: SearXngCategory; label: string; hint: string }[] = [
  { value: "general", label: "General", hint: "Default web results" },
  { value: "news", label: "News", hint: "News engines" },
  { value: "science", label: "Science", hint: "Academic and science-focused engines" },
  { value: "it", label: "IT", hint: "Developer and technology engines" },
  { value: "images", label: "Images", hint: "Image search results" },
  { value: "videos", label: "Videos", hint: "Video search results" },
  { value: "files", label: "Files", hint: "File and document search" },
  { value: "map", label: "Map", hint: "Map and location results" },
  { value: "music", label: "Music", hint: "Music engines" },
  { value: "social media", label: "Social", hint: "Social media engines" },
]

export function resolveSearchConfig(config: SearchApiConfig): SearchApiConfig {
  const providerConfigs: SearchProviderConfigs = config.providerConfigs ?? {
    ...(config.provider !== "none" && config.provider !== "ollama" && config.provider !== "firecrawl" && config.apiKey
      ? {
          [config.provider]: {
            apiKey: config.apiKey,
            serpApiEngine: config.serpApiEngine,
            searXngUrl: config.searXngUrl,
            searXngCategories: config.searXngCategories,
          },
        }
      : {}),
    ...(config.provider === "searxng" && config.searXngUrl
      ? {
          searxng: {
            searXngUrl: config.searXngUrl,
            searXngCategories: config.searXngCategories,
          },
        }
      : {}),
    ...(config.provider === "ollama" && config.ollamaUrl
      ? {
          ollama: {
            ollamaUrl: config.ollamaUrl,
          },
        }
      : {}),
  }

  const activeProvider = config.provider as SearchProvider
  const activeOverride = activeProvider === "none" ? undefined : providerConfigs[activeProvider]
  const resolvedOllamaUrl =
    activeProvider === "ollama"
      ? activeOverride?.ollamaUrl ?? config.ollamaUrl ?? "https://ollama.com"
      : providerConfigs.ollama?.ollamaUrl ?? "https://ollama.com"

  if (activeProvider === "none") {
    return {
      ...config,
      provider: "none",
      apiKey: "",
      serpApiEngine: config.serpApiEngine ?? providerConfigs.serpapi?.serpApiEngine ?? "google",
      searXngUrl: config.searXngUrl ?? providerConfigs.searxng?.searXngUrl ?? "",
      searXngCategories: config.searXngCategories ?? providerConfigs.searxng?.searXngCategories ?? ["general"],
      ollamaUrl: providerConfigs.ollama?.ollamaUrl ?? "https://ollama.com",
      providerConfigs,
      deepResearchSource: config.deepResearchSource ?? "web",
      anyTxt: normalizeAnyTxtConfig(config.anyTxt),
    }
  }

  return {
    ...config,
    provider: activeProvider,
    apiKey: activeOverride?.apiKey ?? config.apiKey ?? "",
    serpApiEngine: activeOverride?.serpApiEngine ?? config.serpApiEngine ?? "google",
    searXngUrl: activeOverride?.searXngUrl ?? config.searXngUrl ?? "",
    searXngCategories: activeOverride?.searXngCategories ?? config.searXngCategories ?? ["general"],
    ollamaUrl: resolvedOllamaUrl,
    providerConfigs,
    deepResearchSource: config.deepResearchSource ?? "web",
    anyTxt: normalizeAnyTxtConfig(config.anyTxt),
  }
}

export function hasConfiguredSearchProvider(config: SearchApiConfig): boolean {
  const resolved = resolveSearchConfig(config)
  if (resolved.provider === "none") return false
  if (resolved.provider === "searxng") {
    return Boolean(resolved.searXngUrl?.trim())
  }
  if (resolved.provider === "ollama") {
    return Boolean(resolved.apiKey?.trim())
  }
  if (resolved.provider === "firecrawl") {
    return true
  }
  return Boolean(resolved.apiKey?.trim())
}

export function hasConfiguredDeepResearchSources(config: SearchApiConfig): boolean {
  const resolved = resolveSearchConfig(config)
  const source = resolved.deepResearchSource ?? "web"
  const webConfigured = hasConfiguredSearchProvider(resolved)
  const anyTxtConfigured = hasConfiguredAnyTxt(resolved.anyTxt)

  if (source === "web") return webConfigured
  if (source === "anytxt") return anyTxtConfigured
  return webConfigured || anyTxtConfigured
}

export async function webSearch(
  query: string,
  config: SearchApiConfig,
  maxResults: number = 10,
): Promise<WebSearchResult[]> {
  const resolved = resolveSearchConfig(config)
  if (resolved.provider === "none") {
    throw new Error("Web search not configured. Select a search provider in Settings.")
  }
  if (
    (resolved.provider === "tavily" || resolved.provider === "serpapi" || resolved.provider === "brave") &&
    !resolved.apiKey
  ) {
    throw new Error("Web search not configured. Add a Tavily, SerpApi, or Brave Search API key in Settings, or select a key-free provider such as Firecrawl or SearXNG.")
  }
  if (resolved.provider === "searxng" && !resolved.searXngUrl?.trim()) {
    throw new Error("Web search not configured. Add a SearXNG instance URL in Settings.")
  }
  if (resolved.provider === "ollama" && !resolved.apiKey?.trim()) {
    throw new Error("Ollama Web Search API requires an Ollama API key. Add one in Settings.")
  }

  switch (resolved.provider) {
    case "tavily":
      return tavilySearch(query, resolved.apiKey, maxResults)
    case "serpapi":
      return serpApiSearch(query, resolved.apiKey, maxResults, resolved.serpApiEngine ?? "google")
    case "searxng":
      return searXngSearch(query, resolved.searXngUrl ?? "", maxResults, resolved.searXngCategories ?? ["general"])
    case "ollama":
      return ollamaSearch(query, resolved.apiKey ?? "", maxResults)
    case "brave":
      return braveSearch(query, resolved.apiKey, maxResults)
    case "firecrawl":
      return firecrawlSearch(query, maxResults)
    default:
      throw new Error(`Unknown search provider: ${resolved.provider}`)
  }
}

function searXngSearchUrl(instanceUrl: string): URL {
  const trimmed = instanceUrl.trim()
  const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`
  const url = new URL(withProtocol)
  const path = url.pathname.replace(/\/+$/, "")
  url.pathname = path.endsWith("/search") || path === "/search"
    ? path
    : `${path}/search`
  url.search = ""
  url.hash = ""
  return url
}

async function searXngSearch(
  query: string,
  instanceUrl: string,
  maxResults: number,
  categories: SearXngCategory[],
): Promise<WebSearchResult[]> {
  let endpoint: URL
  try {
    endpoint = searXngSearchUrl(instanceUrl)
  } catch {
    throw new Error("Invalid SearXNG instance URL. Use a valid http(s) URL, for example https://search.example.com.")
  }

  endpoint.searchParams.set("q", query)
  endpoint.searchParams.set("format", "json")
  endpoint.searchParams.set("categories", (categories.length > 0 ? categories : ["general"]).join(","))

  const httpFetch = await getHttpFetch()
  let response: Response
  try {
    response = await httpFetch(endpoint.toString(), {
      method: "GET",
      headers: { Accept: "application/json" },
    })
  } catch (err) {
    if (isFetchNetworkError(err)) {
      throw new Error(
        "Network error reaching the SearXNG instance. Check the instance URL and whether JSON search is enabled.",
      )
    }
    throw err
  }

  if (!response.ok) {
    const errorText = await response.text().catch(() => "Unknown error")
    throw new Error(`SearXNG search failed (${response.status}): ${errorText}`)
  }

  const data = await response.json()
  return normalizeSearXngResults(data, maxResults)
}

function normalizeSearXngResults(data: { results?: unknown[] }, maxResults: number): WebSearchResult[] {
  return (data.results ?? [])
    .slice(0, maxResults)
    .map((item) => normalizeSearXngResult(item))
    .filter((item) => item.url.length > 0)
}

function normalizeSearXngResult(item: unknown): WebSearchResult {
  const r = item as {
    title?: string
    url?: string
    content?: string
    engine?: string
    category?: string
  }
  const url = r.url ?? ""
  return {
    title: r.title ?? "Untitled",
    url,
    snippet: r.content ?? "",
    source: hostnameFromUrl(url) || r.engine || r.category || "",
  }
}

function hostnameFromUrl(url: string): string {
  try {
    return new URL(url).hostname.replace("www.", "")
  } catch {
    return ""
  }
}

async function tavilySearch(
  query: string,
  apiKey: string,
  maxResults: number,
): Promise<WebSearchResult[]> {
  // Route through the Tauri HTTP plugin so future non-Tavily search
  // providers (Serper, Exa, Brave, Google CSE, ...) with less friendly
  // CORS don't each need their own workaround. See tauri-fetch.ts.
  const httpFetch = await getHttpFetch()
  let response: Response
  try {
    response = await httpFetch("https://api.tavily.com/search", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        api_key: apiKey,
        query,
        max_results: maxResults,
        search_depth: "advanced",
        include_answer: false,
      }),
    })
  } catch (err) {
    if (isFetchNetworkError(err)) {
      throw new Error(
        "Network error reaching api.tavily.com. Check your connectivity and whether the Tavily API key is still valid.",
      )
    }
    throw err
  }

  if (!response.ok) {
    const errorText = await response.text().catch(() => "Unknown error")
    throw new Error(`Tavily search failed (${response.status}): ${errorText}`)
  }

  const data = await response.json()

  return (data.results ?? []).map((r: { title: string; url: string; content: string }) => ({
    title: r.title ?? "Untitled",
    url: r.url ?? "",
    snippet: r.content ?? "",
    source: hostnameFromUrl(r.url ?? ""),
  }))
}

interface FirecrawlSearchResponse {
  success?: boolean
  error?: string
  data?: unknown
  results?: unknown
}

async function firecrawlSearch(
  query: string,
  maxResults: number,
): Promise<WebSearchResult[]> {
  const httpFetch = await getHttpFetch()
  let response: Response
  try {
    response = await httpFetch("https://api.firecrawl.dev/v2/search", {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        query,
        limit: maxResults,
      }),
    })
  } catch (err) {
    if (isFetchNetworkError(err)) {
      throw new Error(
        "Network error reaching Firecrawl Search. Check your connectivity or choose another Web Search provider.",
      )
    }
    throw err
  }

  const text = await response.text().catch(() => "")
  let data: FirecrawlSearchResponse = {}
  try {
    data = text ? JSON.parse(text) as FirecrawlSearchResponse : {}
  } catch {
    if (!response.ok) {
      throw new Error(`Firecrawl search failed (${response.status}): ${text || "Unknown error"}`)
    }
    throw new Error("Firecrawl search returned an invalid JSON response.")
  }

  if (!response.ok) {
    throw new Error(friendlyFirecrawlError(data.error) ?? `Firecrawl search failed (${response.status}): ${text || "Unknown error"}`)
  }

  if (data.success === false || (data.success !== true && typeof data.error === "string" && data.error.trim())) {
    throw new Error(friendlyFirecrawlError(data.error) ?? `Firecrawl search failed: ${data.error ?? "Unknown error"}`)
  }

  return normalizeFirecrawlResults(data, maxResults)
}

function friendlyFirecrawlError(error?: string): string | null {
  const message = error?.trim()
  if (!message) return null
  if (/suspicious|without an API key|firecrawl can't be used without an api key/i.test(message)) {
    return "Firecrawl anonymous search is blocked for this IP. Firecrawl says this network looks suspicious; choose another Web Search provider or try a different network."
  }
  return `Firecrawl search failed: ${message}`
}

function normalizeFirecrawlResults(data: FirecrawlSearchResponse, maxResults: number): WebSearchResult[] {
  const rawResults = extractFirecrawlResultArray(data)
  return rawResults
    .slice(0, maxResults)
    .map((item) => normalizeFirecrawlResult(item))
    .filter((item) => item.url.length > 0)
}

function extractFirecrawlResultArray(data: FirecrawlSearchResponse): unknown[] {
  if (Array.isArray(data.data)) return data.data
  if (Array.isArray(data.results)) return data.results

  return extractFirecrawlNestedResultArray(data.data)
    ?? extractFirecrawlNestedResultArray(data.results)
    ?? []
}

function extractFirecrawlNestedResultArray(value: unknown): unknown[] | null {
  if (!value || typeof value !== "object") return null
  const nested = value as Record<string, unknown>

  // Firecrawl v2 may return categorized results as data.web/news/images.
  // LLM Wiki consumes the first supported text-search category; image/news
  // categories are intentionally ignored because their payloads differ.
  // The first matching category wins even when it is empty.
  for (const key of ["web", "results", "items"]) {
    const nestedValue = nested[key]
    if (Array.isArray(nestedValue)) return nestedValue
  }

  return null
}

function normalizeFirecrawlResult(item: unknown): WebSearchResult {
  if (!item || typeof item !== "object") {
    return { title: "Untitled", url: "", snippet: "", source: "" }
  }
  const r = item as {
    title?: string
    url?: string
    link?: string
    markdown?: string
    content?: string
    description?: string
    snippet?: string
    source?: string
    metadata?: {
      title?: string
      sourceURL?: string
      url?: string
      description?: string
    }
  }
  const url = r.url ?? r.link ?? r.metadata?.sourceURL ?? r.metadata?.url ?? ""
  return {
    title: r.title ?? r.metadata?.title ?? "Untitled",
    url,
    snippet: r.snippet ?? r.description ?? r.metadata?.description ?? r.content ?? r.markdown ?? "",
    source: hostnameFromUrl(url) || r.source || "",
  }
}

async function serpApiSearch(
  query: string,
  apiKey: string,
  maxResults: number,
  engine: SerpApiEngine,
): Promise<WebSearchResult[]> {
  const params = new URLSearchParams({
    engine,
    q: query,
    api_key: apiKey,
    num: String(maxResults),
  })

  const httpFetch = await getHttpFetch()
  let response: Response
  try {
    response = await httpFetch(`https://serpapi.com/search?${params.toString()}`, {
      method: "GET",
      headers: { Accept: "application/json" },
    })
  } catch (err) {
    if (isFetchNetworkError(err)) {
      throw new Error(
        "Network error reaching serpapi.com. Check your connectivity and whether the SerpApi API key is still valid.",
      )
    }
    throw err
  }

  if (!response.ok) {
    const errorText = await response.text().catch(() => "Unknown error")
    throw new Error(`SerpApi search failed (${response.status}): ${errorText}`)
  }

  const data = await response.json()
  if (typeof data.error === "string" && data.error.trim()) {
    throw new Error(`SerpApi search failed: ${data.error}`)
  }

  return normalizeSerpApiResults(data, maxResults)
}

function normalizeSerpApiResults(data: {
  organic_results?: unknown[]
  news_results?: unknown[]
  images_results?: unknown[]
  video_results?: unknown[]
  videos_results?: unknown[]
  shopping_results?: unknown[]
}, maxResults: number): WebSearchResult[] {
  const rawResults =
    data.organic_results ??
    data.news_results ??
    data.images_results ??
    data.video_results ??
    data.videos_results ??
    data.shopping_results ??
    []

  return rawResults
    .slice(0, maxResults)
    .map((item) => normalizeSerpApiResult(item))
}

function normalizeSerpApiResult(item: unknown): WebSearchResult {
  const r = item as {
    title?: string
    link?: string
    url?: string
    source?: string
    snippet?: string
    summary?: string
    description?: string
    thumbnail?: string
    original?: string
    displayed_link?: string
  }
  const url = r.link ?? r.url ?? r.original ?? r.thumbnail ?? ""
  return {
    title: r.title ?? "Untitled",
    url,
    snippet: r.snippet ?? r.summary ?? r.description ?? "",
    source: hostnameFromUrl(url) || r.source || r.displayed_link || "",
  }
}

interface OllamaSearchResponse {
  results?: Array<{
    title?: string
    url?: string
    content?: string
  }>
  error?: string
}

async function ollamaSearch(
  query: string,
  apiKey: string,
  maxResults: number,
): Promise<WebSearchResult[]> {
  const trimmedApiKey = apiKey.trim()
  if (!trimmedApiKey) {
    throw new Error("Ollama Web Search API requires an Ollama API key. Add one in Settings.")
  }

  const headers: Record<string, string> = {
    Accept: "application/json",
    "Content-Type": "application/json",
    Authorization: `Bearer ${trimmedApiKey}`,
  }

  const httpFetch = await getHttpFetch()
  let response: Response
  try {
    response = await httpFetch("https://ollama.com/api/web_search", {
      method: "POST",
      headers,
      body: JSON.stringify({
        query,
        max_results: maxResults,
      }),
    })
  } catch (err) {
    if (isFetchNetworkError(err)) {
      throw new Error(
        "Network error reaching the Ollama Web Search API. Check your connectivity and whether the Ollama API key is still valid.",
      )
    }
    throw err
  }

  if (!response.ok) {
    if (response.status === 401) {
      throw new Error(
        "Ollama Web Search API authentication failed. Check your Ollama API key.",
      )
    }
    const errorText = await response.text().catch(() => "Unknown error")
    throw new Error(`Ollama web search failed (${response.status}): ${errorText}`)
  }

  const data = (await response.json()) as OllamaSearchResponse

  if (data.error) {
    throw new Error(`Ollama web search error: ${data.error}`)
  }

  return (data.results ?? [])
    .slice(0, maxResults)
    .map((r) => {
      const url = r.url ?? ""
      return {
        title: r.title ?? "Untitled",
        url,
        snippet: r.content ?? "",
        source: hostnameFromUrl(url),
      }
    })
}

interface BraveSearchResponse {
  web?: {
    results?: Array<{
      title?: string
      url?: string
      description?: string
    }>
  }
  message?: string
}

async function braveSearch(
  query: string,
  apiKey: string,
  maxResults: number,
): Promise<WebSearchResult[]> {
  // Brave Web Search API caps `count` at 20 per request. Higher values
  // are silently clamped server-side; bound here so the URL stays
  // honest and we don't surprise users by appearing to ask for more.
  const count = Math.max(1, Math.min(maxResults, 20))
  const params = new URLSearchParams({ q: query, count: String(count) })

  const httpFetch = await getHttpFetch()
  let response: Response
  try {
    response = await httpFetch(
      `https://api.search.brave.com/res/v1/web/search?${params.toString()}`,
      {
        method: "GET",
        headers: {
          Accept: "application/json",
          "X-Subscription-Token": apiKey,
        },
      },
    )
  } catch (err) {
    if (isFetchNetworkError(err)) {
      throw new Error(
        "Network error reaching api.search.brave.com. Check your connectivity and whether the Brave Search API key is still valid.",
      )
    }
    throw err
  }

  if (!response.ok) {
    if (response.status === 401 || response.status === 403) {
      throw new Error(
        "Brave Search API authentication failed. Check your subscription token in Settings.",
      )
    }
    const errorText = await response.text().catch(() => "Unknown error")
    throw new Error(`Brave search failed (${response.status}): ${errorText}`)
  }

  const data = (await response.json()) as BraveSearchResponse
  if (data.message && !data.web) {
    throw new Error(`Brave search error: ${data.message}`)
  }

  return (data.web?.results ?? [])
    .slice(0, maxResults)
    .map((r) => {
      const url = r.url ?? ""
      return {
        title: r.title ?? "Untitled",
        url,
        snippet: r.description ?? "",
        source: hostnameFromUrl(url),
      }
    })
}
