///|
/// JavaScript fetch() wrapper for JS target

///|
/// Optionally disable TLS certificate verification for Node.js.
/// This is gated behind CRATER_ALLOW_INSECURE_TLS=1 so normal test runs
/// do not trigger Node's insecure TLS warning.
extern "js" fn js_disable_tls_verify() -> Unit =
  #| () => {
  #|   if (
  #|     typeof process !== 'undefined' &&
  #|     process.env &&
  #|     process.env.CRATER_ALLOW_INSECURE_TLS === '1'
  #|   ) {
  #|     process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
  #|   }
  #| }

///|
fn init {
  js_disable_tls_verify()
}

///|
/// Internal: fetch response as JSON string
/// Binary content types (image/*) are returned with base64-encoded body
extern "js" fn js_fetch_internal(
  url : String,
  http_method : String,
  headers_json : String,
  body_json : String,
  timeout_ms : Int,
) -> @js_async.Promise[String] =
  #| async (url, method, headersJson, bodyJson, timeoutMs) => {
  #|   const headers = JSON.parse(headersJson);
  #|   const body = JSON.parse(bodyJson);
  #|   const controller = new AbortController();
  #|   const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
  #|   try {
  #|     const init = { method, headers, signal: controller.signal };
  #|     if (method !== 'GET' && method !== 'HEAD' && body.length > 0) {
  #|       init.body = body;
  #|     }
  #|     const res = await fetch(url, init);
  #|     clearTimeout(timeoutId);
  #|     const resHeaders = {};
  #|     res.headers.forEach((v, k) => { resHeaders[k] = v; });
  #|     const ct = (resHeaders['content-type'] || '').toLowerCase();
  #|     let responseBody;
  #|     if (ct.startsWith('image/') && !ct.includes('svg')) {
  #|       const buf = await res.arrayBuffer();
  #|       const bytes = new Uint8Array(buf);
  #|       let binary = '';
  #|       for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
  #|       responseBody = 'base64:' + btoa(binary);
  #|     } else {
  #|       responseBody = await res.text();
  #|     }
  #|     return JSON.stringify({ status: res.status, headers: resHeaders, body: responseBody });
  #|   } catch (e) {
  #|     clearTimeout(timeoutId);
  #|     if (e.name === 'AbortError') {
  #|       throw new Error('TIMEOUT');
  #|     }
  #|     throw e;
  #|   }
  #| }

///|
fn escape_json_string(s : String) -> String {
  let buf = StringBuilder::new()
  for c in s {
    match c {
      '"' => buf.write_string("\\\"")
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
fn body_to_json(body : String) -> String {
  "\"" + escape_json_string(body) + "\""
}

///|
/// Parse JSON response into HttpResponse
#warnings("-unused_error_type")
fn parse_response_json(json : String) -> HttpResponse raise HttpError {
  // Simple JSON parsing for response
  // Format: {"status":200,"headers":{"key":"value"},"body":"..."}
  let status = extract_json_int(json, "status")
  let body = extract_json_string(json, "body")
  let headers = parse_headers_json(json)
  { status, headers, body }
}

///|
/// Parse headers object from JSON response
fn parse_headers_json(json_str : String) -> Map[String, String] {
  let headers : Map[String, String] = {}
  if json_str.length() == 0 || json_str == "{}" {
    return headers
  }
  let json = @json.parse(json_str) catch { _ => return headers }
  match json {
    Object(map) =>
      match map.get("headers") {
        Some(Object(hmap)) =>
          hmap.each(fn(k, v) {
            match v {
              String(s) => headers[k.to_lower()] = s
              _ => ()
            }
          })
        _ => ()
      }
    _ => ()
  }
  headers
}

///|
/// Extract integer value from JSON by key
fn extract_json_int(json : String, key : String) -> Int {
  let pattern = "\"" + key + "\":"
  match json.find(pattern) {
    Some(idx) => {
      let start = idx + pattern.length()
      let mut end = start
      let chars = json.to_array()
      while end < chars.length() && chars[end] >= '0' && chars[end] <= '9' {
        end = end + 1
      }
      try {
        let num_str = json.unsafe_substring(start~, end~)
        @string.parse_int(num_str)
      } catch {
        _ => 0
      }
    }
    None => 0
  }
}

///|
/// Extract string value from JSON by key
fn extract_json_string(json : String, key : String) -> String {
  let pattern = "\"" + key + "\":\""
  match json.find(pattern) {
    Some(idx) => {
      let start = idx + pattern.length()
      let mut end = start
      let chars = json.to_array()
      while end < chars.length() {
        if chars[end] == '"' && (end == start || chars[end - 1] != '\\') {
          break
        }
        end = end + 1
      }
      unescape_json_string(json.unsafe_substring(start~, end~))
    }
    None => ""
  }
}

///|
/// Unescape JSON string
fn unescape_json_string(s : String) -> String {
  let buf = StringBuilder::new()
  let chars = s.to_array()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '\\' && i + 1 < chars.length() {
      match chars[i + 1] {
        'n' => buf.write_char('\n')
        'r' => buf.write_char('\r')
        't' => buf.write_char('\t')
        '"' => buf.write_char('"')
        '\\' => buf.write_char('\\')
        c => {
          buf.write_char('\\')
          buf.write_char(c)
        }
      }
      i = i + 2
    } else {
      buf.write_char(chars[i])
      i = i + 1
    }
  }
  buf.to_string()
}

///|
/// Fetch URL using JavaScript fetch() API
pub async fn fetch(
  url : String,
  options? : FetchOptions = FetchOptions::default(),
) -> HttpResponse raise HttpError {
  let prepared = prepare_fetch_options(url, options)
  let headers_json = headers_to_json(prepared.headers)
  let body_json = body_to_json(prepared.body)
  let controller = @js_async.AbortController::new()
  let promise = js_fetch_internal(
    url,
    prepared.http_method,
    headers_json,
    body_json,
    prepared.timeout_ms,
  )
  let result_json = @js_async.Promise::wait(
    promise,
    abort_controller=controller,
  ) catch {
    err => {
      let msg = err.to_string()
      if msg.contains("TIMEOUT") {
        raise TimeoutError
      }
      raise NetworkError(msg)
    }
  }
  let response = parse_response_json(result_json)
  enforce_cors_response(url, prepared, response)
}