import net from 'node:net';
import { getSecret } from '../../secrets.js';
import { wrapFetch } from '../../egress.js';

const MAX_FETCH_BYTES = 512000;

function env(name) {
  return String(process.env[name] || '').trim();
}

/**
 * Resolve a credential: vault first, then process.env.
 * Used for web provider credentials (FIRECRAWL_API_KEY, BRAVE_SEARCH_API_KEY, etc.).
 * Returns empty string if not configured, matching the existing env() contract.
 */
function credential(name) {
  try {
    const secret = getSecret(name);
    if (secret) return String(secret).trim();
  } catch {
    // Vault not ready — fall through to process.env.
  }
  return env(name);
}

export function resolveWebProvider() {
  const configured = env('MADARX_WEB_PROVIDER').toLowerCase();
  if (configured) {
    if (['firecrawl', 'searxng', 'brave', 'fetch'].includes(configured)) return configured;
    throw new Error(`Unsupported MADARX_WEB_PROVIDER: ${configured}`);
  }
  if (credential('FIRECRAWL_API_URL') || credential('FIRECRAWL_API_KEY')) return 'firecrawl';
  if (credential('SEARXNG_URL')) return 'searxng';
  if (credential('BRAVE_SEARCH_API_KEY')) return 'brave';
  return 'fetch';
}

function searchQuery(input = {}) {
  const query = String(input.query || input.q || '').trim();
  if (!query) throw new Error('query is required');
  return query;
}

function searchCount(input = {}) {
  return Math.min(Math.max(Number(input.count || 5), 1), 10);
}

function normalizeResult({ title, url, snippet, source, rank = 0, metadata = {} }) {
  return {
    rank: rank + 1,
    title: String(title || ''),
    url: String(url || ''),
    snippet: String(snippet || ''),
    source,
    metadata,
  };
}

async function jsonRequest(url, init = {}, fetchFn = fetch) {
  const res = await fetchFn(url, init);
  if (!res.ok) {
    let detail = res.statusText || String(res.status);
    try {
      const text = await res.text();
      if (text) detail = `${detail}: ${text.slice(0, 200)}`;
    } catch {
      // Keep the status-only error if the response body cannot be read.
    }
    throw new Error(`Request failed: ${res.status} ${detail}`.trim());
  }
  return res.json();
}

function normalizeBraveResult(item = {}, rank = 0) {
  return normalizeResult({
    rank,
    title: item.title,
    url: item.url,
    snippet: item.description || item.snippet,
    source: 'brave',
    metadata: {
      age: item.age || null,
      family_friendly: item.family_friendly ?? null,
    },
  });
}

function firecrawlHeaders() {
  const headers = {
    Accept: 'application/json',
    'Content-Type': 'application/json',
  };
  const key = credential('FIRECRAWL_API_KEY');
  if (key) headers.Authorization = `Bearer ${key}`;
  return headers;
}

function firecrawlBaseUrl() {
  return (credential('FIRECRAWL_API_URL') || 'https://api.firecrawl.dev').replace(/\/+$/g, '');
}

function firecrawlResults(data = {}) {
  if (Array.isArray(data.data)) return data.data;
  if (Array.isArray(data.results)) return data.results;
  if (Array.isArray(data.web)) return data.web;
  if (Array.isArray(data.web?.results)) return data.web.results;
  return [];
}

async function searchWithFirecrawl(input = {}, fetchFn = fetch) {
  const query = searchQuery(input);
  const count = searchCount(input);
  const data = await jsonRequest(`${firecrawlBaseUrl()}/v1/search`, {
    method: 'POST',
    headers: firecrawlHeaders(),
    body: JSON.stringify({ query, limit: count }),
  }, fetchFn);
  return {
    query,
    source: 'firecrawl',
    results: firecrawlResults(data).slice(0, count).map((item, rank) => normalizeResult({
      rank,
      title: item.title,
      url: item.url || item.link,
      snippet: item.description || item.snippet || item.content || item.markdown,
      source: 'firecrawl',
      metadata: item.metadata || {},
    })).filter(item => item.url),
  };
}

async function searchWithSearxng(input = {}, fetchFn = fetch) {
  const query = searchQuery(input);
  const count = searchCount(input);
  const base = credential('SEARXNG_URL');
  if (!base) throw new Error('SEARXNG_URL not configured');
  const url = new URL('/search', base.replace(/\/+$/g, ''));
  url.searchParams.set('q', query);
  url.searchParams.set('format', 'json');
  if (input.search_lang || input.language) url.searchParams.set('language', String(input.search_lang || input.language));
  const data = await jsonRequest(url, {
    headers: { Accept: 'application/json' },
  }, fetchFn);
  return {
    query,
    source: 'searxng',
    results: (data.results || []).slice(0, count).map((item, rank) => normalizeResult({
      rank,
      title: item.title,
      url: item.url,
      snippet: item.content || item.snippet,
      source: 'searxng',
      metadata: item.metadata || {},
    })).filter(item => item.url),
  };
}

async function searchWithBrave(input = {}, fetchFn = fetch) {
  const key = credential('BRAVE_SEARCH_API_KEY');
  if (!key) throw new Error('BRAVE_SEARCH_API_KEY not configured');
  const query = searchQuery(input);
  const count = searchCount(input);
  const url = new URL('https://api.search.brave.com/res/v1/web/search');
  url.searchParams.set('q', query);
  url.searchParams.set('count', String(count));
  if (input.country) url.searchParams.set('country', String(input.country));
  if (input.search_lang) url.searchParams.set('search_lang', String(input.search_lang));

  const data = await jsonRequest(url, {
    headers: {
      Accept: 'application/json',
      'X-Subscription-Token': key,
    },
  }, fetchFn);
  const results = (data.web?.results || []).slice(0, count).map(normalizeBraveResult);
  return {
    query,
    source: 'brave',
    results,
  };
}

function decodeHtml(value = '') {
  return String(value)
    .replace(/&nbsp;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&#x27;/g, "'");
}

function stripTags(value = '') {
  return decodeHtml(String(value).replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim();
}

function attrValue(tag, name) {
  const match = tag.match(new RegExp(`${name}\\s*=\\s*["']([^"']+)["']`, 'i'));
  return match ? decodeHtml(match[1]) : '';
}

function duckDuckGoResultUrl(rawUrl = '') {
  try {
    const url = new URL(rawUrl, 'https://lite.duckduckgo.com');
    const redirected = url.searchParams.get('uddg');
    if (redirected) return redirected;
    if (['http:', 'https:'].includes(url.protocol)) return url.toString();
  } catch {
    return rawUrl;
  }
  return rawUrl;
}

function parseDuckDuckGoLite(html = '', count = 5) {
  const anchors = [...String(html).matchAll(/<a\b[^>]*href=["'][^"']+["'][^>]*>[\s\S]*?<\/a>/gi)];
  const snippets = [...String(html).matchAll(/<td\b[^>]*class=["'][^"']*result-snippet[^"']*["'][^>]*>([\s\S]*?)<\/td>/gi)]
    .map(match => stripTags(match[1]));
  const results = [];
  for (const anchor of anchors) {
    const tag = anchor[0].match(/<a\b[^>]*>/i)?.[0] || '';
    const title = stripTags(anchor[0]);
    const url = duckDuckGoResultUrl(attrValue(tag, 'href'));
    if (!title || !/^https?:\/\//i.test(url)) continue;
    results.push(normalizeResult({
      rank: results.length,
      title,
      url,
      snippet: snippets[results.length] || '',
      source: 'duckduckgo_lite',
      metadata: {},
    }));
    if (results.length >= count) break;
  }
  return results;
}

async function searchWithFetchFallback(input = {}, fetchFn = fetch) {
  const query = searchQuery(input);
  const count = searchCount(input);
  const url = new URL('https://lite.duckduckgo.com/lite/');
  url.searchParams.set('q', query);
  const res = await fetchFn(url, {
    headers: {
      Accept: 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.2',
      'User-Agent': 'MadarxOS/1.0 (+local research tool)',
    },
  });
  if (!res.ok) throw new Error(`DuckDuckGo Lite search failed: ${res.status}`);
  const html = await res.text();
  return {
    query,
    source: 'fetch',
    results: parseDuckDuckGoLite(html, count),
  };
}

export async function webSearch(input = {}, options = {}) {
  const agentId = options.agent_id ?? null;
  const provider = resolveWebProvider();
  if (provider === 'firecrawl') return searchWithFirecrawl(input, wrapFetch('web_search', agentId));
  if (provider === 'searxng') return searchWithSearxng(input, wrapFetch('web_search', agentId));
  if (provider === 'brave') return searchWithBrave(input, wrapFetch('web_search', agentId));
  return searchWithFetchFallback(input, wrapFetch('web_search', agentId));
}

function ipv4Private(host) {
  const parts = host.split('.').map(Number);
  if (parts.length !== 4 || parts.some(n => !Number.isInteger(n) || n < 0 || n > 255)) return false;
  const [a, b] = parts;
  return a === 10
    || a === 127
    || (a === 169 && b === 254)
    || (a === 172 && b >= 16 && b <= 31)
    || (a === 192 && b === 168)
    || a === 0;
}

function ipv6Private(host) {
  const h = host.toLowerCase();
  return h === '::1'
    || h.startsWith('fc')
    || h.startsWith('fd')
    || h.startsWith('fe80:')
    || h === '::'
    || h.startsWith('0:');
}

export function assertPublicHttpUrl(rawUrl) {
  let url;
  try {
    url = new URL(String(rawUrl || ''));
  } catch {
    throw new Error('url must be a valid URL');
  }
  if (!['http:', 'https:'].includes(url.protocol)) throw new Error('Only http and https URLs are supported');
  const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
  if (!host) throw new Error('URL host is required');
  if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local')) {
    throw new Error('Local/private network URLs are blocked');
  }
  const ipVersion = net.isIP(host);
  if ((ipVersion === 4 && ipv4Private(host)) || (ipVersion === 6 && ipv6Private(host))) {
    throw new Error('Local/private network URLs are blocked');
  }
  return url;
}

function htmlToText(html) {
  return String(html || '')
    .replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, ' ')
    .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, ' ')
    .replace(/<noscript\b[^>]*>[\s\S]*?<\/noscript>/gi, ' ')
    .replace(/<!--[\s\S]*?-->/g, ' ')
    .replace(/<\/(p|div|section|article|header|footer|main|li|h[1-6]|tr)>/gi, '\n')
    .replace(/<[^>]+>/g, ' ')
    .replace(/&nbsp;/g, ' ')
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/[ \t]+\n/g, '\n')
    .replace(/\n{3,}/g, '\n\n')
    .replace(/[ \t]{2,}/g, ' ')
    .trim();
}

export async function webFetch(input = {}, options = {}) {
  const agentId = options.agent_id ?? null;
  const wrappedFetch = wrapFetch('web_fetch', agentId);
  const url = assertPublicHttpUrl(input.url);
  const maxBytes = Math.min(Math.max(Number(input.max_bytes || MAX_FETCH_BYTES), 1024), MAX_FETCH_BYTES);
  const res = await wrappedFetch(url, {
    headers: {
      Accept: 'text/html,text/plain,application/xhtml+xml,application/json;q=0.8,*/*;q=0.2',
      'User-Agent': 'MadarxOS/1.0 (+local research tool)',
    },
    redirect: 'follow',
  });
  if (!res.ok) throw new Error(`Fetch failed: ${res.status}`);
  const length = Number(res.headers.get('content-length') || 0);
  if (length && length > maxBytes) throw new Error(`Response too large: ${length} bytes`);

  const reader = res.body?.getReader();
  if (!reader) {
    const text = await res.text();
    if (Buffer.byteLength(text, 'utf8') > maxBytes) throw new Error('Response too large');
    return { url: url.toString(), status: res.status, content_type: res.headers.get('content-type') || '', text: htmlToText(text) };
  }

  const chunks = [];
  let total = 0;
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    total += value.byteLength;
    if (total > maxBytes) throw new Error('Response too large');
    chunks.push(value);
  }
  const body = Buffer.concat(chunks).toString('utf8');
  const contentType = res.headers.get('content-type') || '';
  return {
    url: res.url || url.toString(),
    status: res.status,
    content_type: contentType,
    text: htmlToText(body).slice(0, Math.min(maxBytes, 120000)),
    source: { url: res.url || url.toString(), fetched_at: new Date().toISOString() },
  };
}

export async function webExtract(input = {}, options = {}) {
  const agentId = options.agent_id ?? null;
  const url = assertPublicHttpUrl(input.url);
  if (!credential('FIRECRAWL_API_URL') && !credential('FIRECRAWL_API_KEY')) {
    return webFetch(input, options);
  }

  const data = await jsonRequest(`${firecrawlBaseUrl()}/v1/scrape`, {
    method: 'POST',
    headers: firecrawlHeaders(),
    body: JSON.stringify({
      url: url.toString(),
      formats: ['markdown'],
    }),
  }, wrapFetch('web_extract', agentId));
  const payload = data.data || data;
  const text = String(payload.markdown || payload.content || payload.text || '');
  return {
    url: url.toString(),
    status: 200,
    content_type: 'text/markdown',
    text: text.slice(0, 120000),
    metadata: payload.metadata || {},
    source: {
      provider: 'firecrawl',
      url: url.toString(),
      fetched_at: new Date().toISOString(),
    },
  };
}
