#!/usr/bin/env python3
"""PRUVA R3B observation-only instrumentation for hermes-agent electron/main.ts.

Adds, gated entirely on the env var HERMES_REPRO_INSTRUMENT_LOG (a JSONL path):

  1. A reproInstrumentLog() helper + top-level main-process observers:
       - app 'browser-window-created'  (any new BrowserWindow, incl. default-allow spawns)
       - app 'web-contents-created'    (guest webContents detection)
       - guest 'did-create-window'     (fires when a guest page's window.open succeeds)
       - guest 'console-message'       (attacker page self-report channel)
       - a RECORDING stub for shell.openExternal (never executes the OS open)
  2. A log line inside the main window's existing setWindowOpenHandler so we can
     see if a guest open ever routes to the main window handler (R2 parity path).
  3. A log line inside openExternalUrl() itself.

No behavior is changed except that shell.openExternal is stubbed to record-only
when instrumentation is active (required safety: never open a real browser).

Idempotent: skips patching when the marker is already present.
"""

import sys

MARKER = "PRUVA-REPRO-INSTRUMENTATION"

HELPER = """
// [PRUVA-REPRO-INSTRUMENTATION] observation-only hooks (gated by env var).
const REPRO_INSTRUMENT_LOG = process.env.HERMES_REPRO_INSTRUMENT_LOG
function reproInstrumentLog(entry) {
  if (!REPRO_INSTRUMENT_LOG) {
    return
  }
  try {
    fs.appendFileSync(REPRO_INSTRUMENT_LOG, JSON.stringify({ t: Date.now(), ...entry }) + '\\n')
  } catch {
    // never let instrumentation break the app
  }
}
if (REPRO_INSTRUMENT_LOG) {
  // RECORD, never execute: a real xdg-open/browser spawn would leave the sandbox.
  shell.openExternal = url => {
    reproInstrumentLog({ event: 'shell.openExternal', url: String(url) })
    return Promise.resolve()
  }
  app.on('browser-window-created', (_event, win) => {
    let url = null
    try {
      url = win.webContents?.getURL?.() ?? null
    } catch {}
    reproInstrumentLog({ event: 'browser-window-created', id: win.id, url })
    try {
      win.webContents.on('did-start-navigation', (_e, navUrl) =>
        reproInstrumentLog({ event: 'win.did-start-navigation', id: win.id, url: navUrl })
      )
      win.webContents.on('did-finish-load', () =>
        reproInstrumentLog({ event: 'win.did-finish-load', id: win.id, url: win.webContents.getURL() })
      )
    } catch {}
  })
  app.on('web-contents-created', (_event, wc) => {
    let type = 'unknown'
    try {
      type = wc.getType()
    } catch {}
    reproInstrumentLog({ event: 'web-contents-created', id: wc.id, type })
    if (type === 'webview') {
      // NOTE: deliberately NOT calling wc.setWindowOpenHandler here — installing a
      // handler would change the exact behavior under test (no handler = default
      // allow in Electron). did-create-window is a passive notification.
      wc.on('did-create-window', (child, details) => {
        reproInstrumentLog({
          event: 'guest.did-create-window',
          guestId: wc.id,
          childId: child?.id ?? null,
          url: details?.url ?? null,
          frameName: details?.frameName ?? null,
          disposition: details?.disposition ?? null,
          options: details?.options ? JSON.stringify(details.options).slice(0, 400) : null
        })
      })
      wc.on('console-message', (_e, level, message, line, sourceId) => {
        reproInstrumentLog({
          event: 'guest.console-message',
          guestId: wc.id,
          level,
          message: String(message).slice(0, 600),
          line,
          sourceId
        })
      })
      wc.on('will-navigate', (_e, navUrl) => {
        reproInstrumentLog({ event: 'guest.will-navigate', guestId: wc.id, url: navUrl })
      })
    }
  })
  reproInstrumentLog({ event: 'instrumentation-installed', electron: process.versions.electron })
}
"""

OPEN_HANDLER_ANCHOR = """  win.webContents.setWindowOpenHandler(details => {
    openExternalUrl(details.url)
"""

OPEN_HANDLER_PATCHED = """  win.webContents.setWindowOpenHandler(details => {
    reproInstrumentLog({
      event: 'main.setWindowOpenHandler',
      url: details.url,
      frameName: details.frameName,
      disposition: details.disposition,
      referrer: details.referrer ? String(details.referrer.url || '') : null
    })
    openExternalUrl(details.url)
"""

OPEN_EXTERNAL_ANCHOR = """function openExternalUrl(rawUrl) {
  const raw = String(rawUrl || '').trim()
"""

OPEN_EXTERNAL_PATCHED = """function openExternalUrl(rawUrl) {
  reproInstrumentLog({ event: 'openExternalUrl', url: String(rawUrl || '') })
  const raw = String(rawUrl || '').trim()
"""

SETNAME_ANCHOR = "app.setName(APP_NAME)"


def main() -> int:
    if len(sys.argv) != 2:
        print("usage: patch_instrumentation.py <path-to-main.ts>", file=sys.stderr)
        return 2

    target = sys.argv[1]
    with open(target, "r", encoding="utf-8") as fh:
        src = fh.read()

    if MARKER in src:
        print(f"already patched: {target}")
        return 0

    if SETNAME_ANCHOR not in src:
        print(f"FATAL: anchor {SETNAME_ANCHOR!r} not found", file=sys.stderr)
        return 1
    if OPEN_HANDLER_ANCHOR not in src:
        print("FATAL: setWindowOpenHandler anchor not found", file=sys.stderr)
        return 1
    if OPEN_EXTERNAL_ANCHOR not in src:
        print("FATAL: openExternalUrl anchor not found", file=sys.stderr)
        return 1

    src = src.replace(SETNAME_ANCHOR, HELPER + "\n" + SETNAME_ANCHOR, 1)
    src = src.replace(OPEN_HANDLER_ANCHOR, OPEN_HANDLER_PATCHED, 1)
    src = src.replace(OPEN_EXTERNAL_ANCHOR, OPEN_EXTERNAL_PATCHED, 1)

    with open(target, "w", encoding="utf-8") as fh:
        fh.write(src)

    print(f"patched: {target}")
    return 0


if __name__ == "__main__":
    sys.exit(main())
