/**
 * browser-fetch.js — headless-style fetch adapter with Chrome 124 headers,
 * in-memory cookie jar, redirect following, multi-mode extraction,
 * and egress logging via wrapFetch.
 *
 * No Playwright dependency. Pure fetch + regex HTML parsing.
 */

import { wrapFetch } from '../../egress.js';
import { redactToolOutput } from '../safety.js';

// ---------------------------------------------------------------------------
// Chrome 124 headers
// ---------------------------------------------------------------------------
const CHROME_HEADERS = {
  'User-Agent':
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
  Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
  'Accept-Language': 'en-US,en;q=0.9',
  'Sec-Fetch-Mode': 'navigate',
  'Sec-Fetch-Site': 'none',
  'Sec-Fetch-User': '?1',
  'Sec-Ch-Ua': '"Chromium";v="124", "Not(A:Brand";v="24"',
  'Upgrade-Insecure-Requests': '1',
};

// ---------------------------------------------------------------------------
// In-memory cookie jar: Map<host, cookieString>
// ---------------------------------------------------------------------------
const cookieJar = new Map();

function getHost(url) {
  try {
    return new URL(url).hostname;
  } catch {
    return url;
  }
}

function captureCookies(host, response) {
  const setCookie = response.headers.get('set-cookie');
  if (!setCookie) return;
  const pairs = setCookie
    .split(/,(?=[^;]+=[^;]+)/)
    .map(s => s.trim().split(';')[0].trim())
    .filter(Boolean);
  const existing = cookieJar.get(host) || '';
  const merged = existing
    ? [...existing.split('; '), ...pairs].filter(Boolean).join('; ')
    : pairs.join('; ');
  cookieJar.set(host, merged);
}

function buildHeaders(url) {
  const host = getHost(url);
  const headers = { ...CHROME_HEADERS };
  const cookies = cookieJar.get(host);
  if (cookies) headers['Cookie'] = cookies;
  return headers;
}

// ---------------------------------------------------------------------------
// Redirect following (up to 5 hops, including meta-refresh)
// ---------------------------------------------------------------------------
const MAX_REDIRECTS = 5;

function extractMetaRefresh(html) {
  const m = html.match(
    /<meta[^>]+http-equiv=["']?refresh["']?[^>]+content=["']?\d+;\s*url=([^"'\s>]+)/i,
  );
  return m ? m[1] : null;
}

async function fetchWithRedirects(startUrl, wrappedFetch) {
  let url = startUrl;
  const visited = [];
  for (let i = 0; i < MAX_REDIRECTS; i++) {
    visited.push(url);
    const host = getHost(url);
    const res = await wrappedFetch(url, {
      method: 'GET',
      redirect: 'manual',
      headers: buildHeaders(url),
      signal: AbortSignal.timeout(20000),
    });
    captureCookies(host, res);

    if (res.status >= 300 && res.status < 400) {
      const location = res.headers.get('location');
      if (!location) break;
      url = new URL(location, url).href;
      continue;
    }

    const html = await res.text();
    const metaUrl = extractMetaRefresh(html);
    if (metaUrl && i < MAX_REDIRECTS - 1) {
      url = new URL(metaUrl, url).href;
      continue;
    }

    return { res, html, finalUrl: url, redirects: visited.length > 1 ? visited : undefined };
  }
  const host = getHost(url);
  const res = await wrappedFetch(url, {
    method: 'GET',
    redirect: 'follow',
    headers: buildHeaders(url),
    signal: AbortSignal.timeout(20000),
  });
  captureCookies(host, res);
  const html = await res.text();
  return { res, html, finalUrl: url, redirects: visited };
}

// ---------------------------------------------------------------------------
// HTML extraction helpers
// ---------------------------------------------------------------------------

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

function toMarkdown(html) {
  let s = html;
  s = s.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, '');
  s = s.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, t) => '\n# ' + stripTags(t) + '\n');
  s = s.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, t) => '\n## ' + stripTags(t) + '\n');
  s = s.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, t) => '\n### ' + stripTags(t) + '\n');
  s = s.replace(/<h[4-6][^>]*>([\s\S]*?)<\/h[4-6]>/gi, (_, t) => '\n#### ' + stripTags(t) + '\n');
  s = s.replace(/<a[^>]+href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => '[' + stripTags(text) + '](' + href + ')');
  s = s.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, (_, t) => '`' + t + '`');
  s = s.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, t) => '\n```\n' + stripTags(t) + '\n```\n');
  s = s.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, t) => '\n- ' + stripTags(t));
  s = s.replace(/<\/?(p|div|br)[^>]*>/gi, '\n');
  s = s.replace(/<[^>]+>/g, ' ');
  s = s
    .replace(/&amp;/g, '&')
    .replace(/&lt;/g, '<')
    .replace(/&gt;/g, '>')
    .replace(/&quot;/g, '"')
    .replace(/&#39;/g, "'")
    .replace(/&nbsp;/g, ' ');
  s = s.replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
  return s;
}

function extractJsonLd(html) {
  const results = [];
  const re = /<script[^>]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
  let m;
  while ((m = re.exec(html)) !== null) {
    try {
      results.push(JSON.parse(m[1]));
    } catch {
      // Skip malformed blocks
    }
  }
  return results;
}

function extractMetadata(html) {
  const meta = {};
  const titleM = html.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
  if (titleM) meta.title = titleM[1].replace(/<[^>]+>/g, '').trim();
  const re = /<meta[^>]+(?:name|property)=["']([^"']+)["'][^>]+content=["']([^"']*)["'][^>]*>/gi;
  let m;
  while ((m = re.exec(html)) !== null) {
    meta[m[1]] = m[2];
  }
  const re2 = /<meta[^>]+content=["']([^"']*)["'][^>]+(?:name|property)=["']([^"']+)["'][^>]*>/gi;
  while ((m = re2.exec(html)) !== null) {
    if (!meta[m[2]]) meta[m[2]] = m[1];
  }
  return meta;
}

// ---------------------------------------------------------------------------
// Simple selector matcher (tag / #id / .class)
// ---------------------------------------------------------------------------
function applySelector(html, selector) {
  if (!selector) return null;
  try {
    let pattern;
    if (selector.startsWith('#')) {
      const id = selector.slice(1).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      pattern = new RegExp('<[a-z][^>]+id=["\']' + id + '["\'][^>]*>([\\s\\S]*?)<\\/[a-z]+>', 'i');
    } else if (selector.startsWith('.')) {
      const cls = selector.slice(1).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      pattern = new RegExp('<[a-z][^>]+class=["\'][^"\']*' + cls + '[^"\']*["\'][^>]*>([\\s\\S]*?)<\\/[a-z]+>', 'i');
    } else {
      const tag = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      pattern = new RegExp('<' + tag + '[^>]*>([\\s\\S]*?)<\\/' + tag + '>', 'i');
    }
    const m = html.match(pattern);
    return m ? m[0] : null;
  } catch {
    return null;
  }
}

// ---------------------------------------------------------------------------
// Main export: browserNavigate
// ---------------------------------------------------------------------------

export async function browserNavigate({
  url,
  extract = 'text',
  selector,
  wait_for,
  max_chars = 12000,
} = {}) {
  const cappedMax = Math.min(Number(max_chars) || 12000, 50000);
  const wrapped = wrapFetch('browser_navigate', null);

  let res, html, finalUrl, redirects;
  try {
    ({ res, html, finalUrl, redirects } = await fetchWithRedirects(url, wrapped));
  } catch (err) {
    return {
      status: 'error',
      url,
      final_url: url,
      content_type: null,
      content: 'Fetch error: ' + String(err && err.message ? err.message : err).slice(0, 512),
      length: 0,
    };
  }

  const contentType = res.headers.get('content-type') || '';

  if (wait_for && !html.includes(wait_for)) {
    return {
      status: 'incomplete',
      url,
      final_url: finalUrl,
      content_type: contentType,
      hint: 'Substring not found, site may need JS rendering',
      length: html.length,
      ...(redirects ? { redirects } : {}),
    };
  }

  let workingHtml = html;
  if (selector) {
    const narrowed = applySelector(html, selector);
    if (narrowed) workingHtml = narrowed;
  }

  let content;
  try {
    if (extract === 'text') {
      content = stripTags(workingHtml).slice(0, cappedMax);
    } else if (extract === 'markdown') {
      content = toMarkdown(workingHtml).slice(0, cappedMax);
    } else if (extract === 'json-ld') {
      content = extractJsonLd(workingHtml);
    } else if (extract === 'metadata') {
      content = extractMetadata(workingHtml);
    } else {
      content = workingHtml.slice(0, cappedMax);
    }
  } catch {
    content = workingHtml.slice(0, cappedMax);
  }

  let safeContent = content;
  if (typeof content === 'string') {
    safeContent = redactToolOutput(content);
  }

  return {
    status: 'ok',
    url,
    final_url: finalUrl,
    content_type: contentType,
    content: safeContent,
    length: typeof html === 'string' ? html.length : 0,
    ...(redirects ? { redirects } : {}),
  };
}

// ---------------------------------------------------------------------------
// browserExtractStructured: json-ld + metadata merged
// ---------------------------------------------------------------------------

export async function browserExtractStructured(url) {
  const [jsonLdResult, metaResult] = await Promise.all([
    browserNavigate({ url, extract: 'json-ld', max_chars: 50000 }),
    browserNavigate({ url, extract: 'metadata', max_chars: 50000 }),
  ]);
  return {
    json_ld: Array.isArray(jsonLdResult.content) ? jsonLdResult.content : [],
    metadata: typeof metaResult.content === 'object' && metaResult.content !== null ? metaResult.content : {},
  };
}
