// Dev-only fetch wrapper. Every HTTP request the RN app makes is
// recorded as one line in the Metro stream (which `tail-logs.sh`
// demultiplexes into `net.log`) and, if the response is JSON below the
// size cap, its body is sunk to `Paths.document/devnet/` for offline
// inspection via `scripts/dev/snapshot.sh`.
//
// Redaction rules:
//   - Authorization headers are never logged (we never read `init?.headers`).
//   - URL access_token query params are masked to `access_token=REDACTED`
//     before logging.
//   - Only the host + path is logged, never the absolute URL with query
//     secrets.
//   - Body sink is gated by content-type (JSON only) and capped at 256 KB.

import { Directory, File, Paths } from 'expo-file-system'
import { devlog } from './log'

const MAX_BODY_BYTES = 256 * 1024
let seq = 0
let installed = false
let dir: Directory | null = null

function devnetDir(): Directory {
  if (!dir) {
    dir = new Directory(Paths.document, 'devnet')
    try {
      dir.create({ intermediates: true, idempotent: true })
    } catch {
      // Directory creation can race or fail under low disk; the next
      // write call will surface the error if it matters.
    }
  }
  return dir
}

function maskSecrets(url: string): string {
  return url.replace(/([?&])access_token=[^&]+/gi, '$1access_token=REDACTED')
}

function slugify(host: string, pathname: string): string {
  return `${host}${pathname}`.replace(/[^a-z0-9._-]+/gi, '_')
}

async function sinkResponseBody(
  n: number,
  host: string,
  pathname: string,
  contentType: string,
  body: string
): Promise<void> {
  if (!contentType.toLowerCase().includes('json')) return
  if (!body || body.length === 0) return
  if (body.length > MAX_BODY_BYTES) return
  try {
    const d = devnetDir()
    const responsesDir = new Directory(d, 'responses')
    responsesDir.create({ intermediates: true, idempotent: true })
    const name = `${String(n).padStart(5, '0')}-${slugify(host, pathname)}.json`
    const file = new File(responsesDir, name)
    if (file.exists) file.delete()
    file.create()
    file.write(body)
  } catch {
    // Best-effort; this is dev observability, never user-visible.
  }
}

export function installNetLog(): void {
  if (!__DEV__) return
  if (installed) return
  installed = true
  const original = global.fetch
  global.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
    const urlStr =
      typeof input === 'string'
        ? input
        : input instanceof URL
          ? input.toString()
          : (input as Request).url
    let host = '?'
    let pathname = ''
    let masked = urlStr
    try {
      const u = new URL(urlStr)
      host = u.host
      pathname = u.pathname
      masked = maskSecrets(urlStr)
    } catch {
      masked = maskSecrets(urlStr)
    }
    const method = init?.method ?? 'GET'
    const n = ++seq
    const t0 = Date.now()
    try {
      const resp = await original(input as any, init)
      const ms = Date.now() - t0
      const ct = resp.headers.get('content-type') ?? ''
      // Clone-then-text so the original response remains consumable by
      // the actual caller. `.clone()` is the standard Fetch API path
      // for this; React Native's fetch implements it.
      let bodyText = ''
      try {
        bodyText = await resp.clone().text()
      } catch {
        bodyText = ''
      }
      devlog.net(
        `#${n} ${method} ${host}${pathname} ${resp.status} ${ms}ms bytes=${bodyText.length}`
      )
      void sinkResponseBody(n, host, pathname, ct, bodyText)
      return resp
    } catch (err) {
      const ms = Date.now() - t0
      const errStr = String(err).slice(0, 200)
      devlog.net(`#${n} ${method} ${host}${pathname} FAIL ${ms}ms ${errStr}`)
      // The masked URL is only retained for the off-chance the operator
      // needs to grep against the redacted form; we still don't log the
      // raw `input` to avoid leaking query secrets.
      void masked
      throw err
    }
  }) as typeof global.fetch
}
