/**
 * browser-session.js — native Playwright browser-interaction adapter.
 *
 * Drives a real headless Chromium so the main agent can navigate, click, type,
 * read, and screenshot pages that need JavaScript (unlike browser_navigate,
 * which is fetch + regex only). One managed persistent context is shared across
 * calls and auto-closed after an idle window so we never leak browser processes.
 *
 * Safety posture:
 *  - Dedicated profile under DATA_DIR/browser-profile — NOT the user's personal
 *    Chrome. Navigations are SSRF-guarded (no localhost / private ranges).
 *  - State-changing actions (browser_act) are risk_level 'high' in the registry,
 *    so they pass through the approval gate in approval_required trust mode.
 *  - No arbitrary JS eval is exposed to the agent.
 *  - Output is capped + redacted.
 *
 * playwright is imported lazily so the registry stays loadable (and the tool can
 * remain enabled_default:false) even when Chromium has not been provisioned.
 */

import path from 'node:path';
import fs from 'node:fs';
import { DATA_DIR } from '../../paths.js';
import { redactToolOutput } from '../safety.js';
import { assertPublicHttpUrl } from './web.js';

const PROFILE_DIR = path.join(DATA_DIR, 'browser-profile');
const SHOTS_DIR = path.join(DATA_DIR, 'browser-screenshots');
const IDLE_CLOSE_MS = 90_000;
const DEFAULT_NAV_TIMEOUT_MS = 30_000;
const MAX_NAV_TIMEOUT_MS = 120_000;
const MAX_TEXT_CHARS = 20_000;

let _context = null;
let _page = null;
let _idleTimer = null;

function clampTimeout(ms) {
  const n = Number(ms);
  if (!Number.isFinite(n) || n <= 0) return DEFAULT_NAV_TIMEOUT_MS;
  return Math.min(MAX_NAV_TIMEOUT_MS, Math.max(1_000, Math.floor(n)));
}

function cap(text) {
  return redactToolOutput(String(text ?? '').slice(0, MAX_TEXT_CHARS));
}

function touchIdleTimer() {
  if (_idleTimer) clearTimeout(_idleTimer);
  _idleTimer = setTimeout(() => { closeBrowser().catch(() => {}); }, IDLE_CLOSE_MS);
  // Do not keep the event loop alive on account of the browser.
  if (typeof _idleTimer.unref === 'function') _idleTimer.unref();
}

export async function closeBrowser() {
  if (_idleTimer) { clearTimeout(_idleTimer); _idleTimer = null; }
  const ctx = _context;
  _context = null;
  _page = null;
  if (ctx) { try { await ctx.close(); } catch { /* already gone */ } }
}

async function ensurePage() {
  if (_context && _page && !_page.isClosed()) {
    touchIdleTimer();
    return _page;
  }
  let chromium;
  try {
    ({ chromium } = await import('playwright'));
  } catch {
    throw new Error(
      'Playwright is not installed. Run: npm install playwright && npx playwright install chromium',
    );
  }
  fs.mkdirSync(PROFILE_DIR, { recursive: true });
  try {
    _context = await chromium.launchPersistentContext(PROFILE_DIR, {
      headless: true,
      args: ['--no-sandbox', '--disable-dev-shm-usage'],
    });
  } catch (err) {
    _context = null;
    throw new Error(
      `Failed to launch Chromium (${err?.message || err}). Provision it with: npx playwright install chromium`,
    );
  }
  _context.setDefaultTimeout(DEFAULT_NAV_TIMEOUT_MS);
  _page = _context.pages()[0] || (await _context.newPage());
  touchIdleTimer();
  return _page;
}

async function pageState(page) {
  let title = '';
  try { title = await page.title(); } catch { /* page may be mid-navigation */ }
  return { url: page.url(), title };
}

// ── browser_open ────────────────────────────────────────────────────────────
export async function browserOpen(input = {}) {
  const url = assertPublicHttpUrl(input.url).href;
  const timeout = clampTimeout(input.timeout_ms);
  try {
    const page = await ensurePage();
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
    if (input.wait_for) {
      try { await page.waitForSelector(String(input.wait_for), { timeout }); }
      catch { /* selector never appeared — return what we have */ }
    }
    const { url: finalUrl, title } = await pageState(page);
    const text = await page.evaluate(() => document.body?.innerText || '');
    return { ok: true, tool: 'browser_open', status: 'complete', url: finalUrl, title, text: cap(text) };
  } catch (err) {
    return { ok: false, tool: 'browser_open', status: 'failed', url, error: String(err?.message || err) };
  }
}

// ── browser_act ───────────────────────────────────────────────────────────────
// State-changing interactions. Locator is a CSS selector, or "text=..." form.
const ACTIONS = new Set(['click', 'fill', 'type', 'select', 'press', 'scroll', 'wait']);

export async function browserAct(input = {}) {
  const action = String(input.action || '').toLowerCase();
  if (!ACTIONS.has(action)) {
    return { ok: false, tool: 'browser_act', status: 'error', error: `unknown action: ${action || '(none)'}` };
  }
  const timeout = clampTimeout(input.timeout_ms);
  let page;
  try { page = await ensurePage(); }
  catch (err) { return { ok: false, tool: 'browser_act', status: 'failed', error: String(err?.message || err) }; }

  try {
    if (action === 'scroll') {
      await page.evaluate(() => window.scrollBy(0, window.innerHeight));
    } else if (action === 'wait') {
      if (input.selector) await page.waitForSelector(String(input.selector), { timeout });
      else await page.waitForTimeout(Math.min(timeout, 10_000));
    } else {
      const selector = String(input.selector || '');
      if (!selector) return { ok: false, tool: 'browser_act', status: 'error', error: 'selector is required' };
      const locator = page.locator(selector).first();
      if (action === 'click') await locator.click({ timeout });
      else if (action === 'fill') await locator.fill(String(input.text ?? ''), { timeout });
      else if (action === 'type') await locator.pressSequentially(String(input.text ?? ''), { timeout });
      else if (action === 'select') await locator.selectOption(String(input.value ?? input.text ?? ''), { timeout });
      else if (action === 'press') await locator.press(String(input.key || 'Enter'), { timeout });
    }
    const { url, title } = await pageState(page);
    return { ok: true, tool: 'browser_act', status: 'complete', action, url, title };
  } catch (err) {
    return { ok: false, tool: 'browser_act', status: 'failed', action, error: String(err?.message || err) };
  }
}

// ── browser_extract ─────────────────────────────────────────────────────────
export async function browserExtract(input = {}) {
  const mode = String(input.mode || 'text').toLowerCase();
  const selector = input.selector ? String(input.selector) : null;
  let page;
  try { page = await ensurePage(); }
  catch (err) { return { ok: false, tool: 'browser_extract', status: 'failed', error: String(err?.message || err) }; }

  try {
    let content = '';
    if (mode === 'html') {
      content = selector
        ? (await page.locator(selector).first().innerHTML().catch(() => '')) || ''
        : await page.content();
    } else if (mode === 'json-ld') {
      const blocks = await page.evaluate(() =>
        Array.from(document.querySelectorAll('script[type="application/ld+json"]')).map(s => s.textContent || ''));
      content = blocks.join('\n');
    } else {
      // text (default)
      content = selector
        ? (await page.locator(selector).first().innerText().catch(() => '')) || ''
        : await page.evaluate(() => document.body?.innerText || '');
    }
    const { url, title } = await pageState(page);
    return { ok: true, tool: 'browser_extract', status: 'complete', mode, url, title, content: cap(content) };
  } catch (err) {
    return { ok: false, tool: 'browser_extract', status: 'failed', error: String(err?.message || err) };
  }
}

// ── browser_screenshot ────────────────────────────────────────────────────────
// Saved to disk (images are useless inline in a text tool loop). Returns path.
export async function browserScreenshot(input = {}) {
  let page;
  try { page = await ensurePage(); }
  catch (err) { return { ok: false, tool: 'browser_screenshot', status: 'failed', error: String(err?.message || err) }; }
  try {
    fs.mkdirSync(SHOTS_DIR, { recursive: true });
    const safe = `shot-${Date.now()}.png`.replace(/[^a-zA-Z0-9_.-]/g, '-');
    const file = path.join(SHOTS_DIR, safe);
    const opts = { path: file, fullPage: Boolean(input.full_page) };
    if (input.selector) {
      await page.locator(String(input.selector)).first().screenshot({ path: file });
    } else {
      await page.screenshot(opts);
    }
    const { url, title } = await pageState(page);
    return { ok: true, tool: 'browser_screenshot', status: 'complete', url, title, path: file };
  } catch (err) {
    return { ok: false, tool: 'browser_screenshot', status: 'failed', error: String(err?.message || err) };
  }
}
