/*
 * This file is part of the Fun programming language.
 * https://fun-lang.xyz/
 *
 * Copyright 2026 Johannes Findeisen <you@hanez.org>
 * Licensed under the terms of the Apache-2.0 license.
 * https://opensource.org/license/apache-2-0
 *
 * Added: 2026-03-25
 */

// Minimal, parser-friendly CGI helper (incrementally extend as needed)

#include <strings.fun>

class CGI()
  fun _construct(this)
    this._params = {}
    this._cookies = {}
    this._headers = {}
    this._header_list = []  // preserve insertion order for emission without map iteration
    this._status = 200
    this._status_text = "OK"
    this._content_type = "text/html; charset=utf-8"

    this.env = {}
    this.env["REQUEST_METHOD"] = env("REQUEST_METHOD")
    this.env["QUERY_STRING"] = env("QUERY_STRING")
    this.env["CONTENT_TYPE"] = env("CONTENT_TYPE")
    this.env["CONTENT_LENGTH"] = env("CONTENT_LENGTH")
    this.env["HTTP_COOKIE"] = env("HTTP_COOKIE")
    this.env["POST_DATA"] = env("POST_DATA")

    // Cookies
    this._cookies = this._parse_cookies(this.env["HTTP_COOKIE"])

    // Params from QUERY_STRING
    qs = this.env["QUERY_STRING"]
    if (typeof(qs) == "String" && len(qs) > 0)
      parsed_qs = this._parse_urlencoded(qs)
      this._merge_params(parsed_qs)

    // Params from POST (x-www-form-urlencoded only)
    ct = this.env["CONTENT_TYPE"]
    pd = this.env["POST_DATA"]
    // Fallback: when running under a real CGI, POST data comes from stdin, not env.
    // Only attempt to read stdin if method is POST, content-length > 0, and POST_DATA is empty.
    if (!(typeof(pd) == "String" && len(pd) > 0))
      if (str_to_upper(this.env["REQUEST_METHOD"]) == "POST")
        cl = to_number(this.env["CONTENT_LENGTH"])
        if (cl > 0)
          // Read up to CONTENT_LENGTH bytes from stdin. Some CGI runners may deliver
          // urlencoded chunks split at '&' without including the delimiter; reconstruct
          // by inserting '&' between successive chunks.
          parts = []
          total = 0
          // Safeguard: cap iterations to avoid infinite loops on unexpected behavior
          iter = 0
          while (total < cl && iter < 100000)
            chunk = input("")
            if (!(typeof(chunk) == "String"))
              break
            if (len(chunk) == 0)
              // EOF without newline
              break
            push(parts, chunk)
            total = total + len(chunk)
            iter = iter + 1
          // Rebuild with '&' between pieces to restore typical form encoding
          if (len(parts) > 0)
            rebuilt = parts[0]
            idx = 1
            np = len(parts)
            while (idx < np)
              rebuilt = rebuilt + "&" + parts[idx]
              idx = idx + 1
            tmp = rebuilt
          else
            tmp = ""
          // Trim to declared content length to drop any stray newline added by the CGI runner
          if (len(tmp) > cl)
            pd = substr(tmp, 0, cl)
          else
            pd = tmp
          this.env["POST_DATA"] = pd
        else
          // No declared length; try to read a single line (common with simple runners)
          tmp = input("")
          if (typeof(tmp) == "String" && len(tmp) > 0)
            this.env["POST_DATA"] = tmp
          // Keep local variable in sync so parsing below sees the data
          pd = this.env["POST_DATA"]
    // Decide whether to treat body as urlencoded: default yes for POST unless explicitly multipart
    is_urlencoded = 1
    if (typeof(ct) == "String" && len(ct) > 0)
      lct = str_to_lower(ct)
      if (find(lct, "multipart/form-data") >= 0)
        is_urlencoded = 0
    if (is_urlencoded)
      if (typeof(pd) == "String" && len(pd) > 0)
        parsed_pd = this._parse_urlencoded(pd)
        this._merge_params(parsed_pd)

  fun param(this, name)
    arr = this._params[name]
    if (typeof(arr) != "Array")
      return ""
    if (len(arr) > 0)
      return arr[0]
    return ""

  fun param_all(this, name)
    arr = this._params[name]
    if (typeof(arr) != "Array")
      return []
    return arr

  fun params(this)
    // Return the internal map directly (no copying, to avoid map iteration)
    return this._params

  fun cookie(this, name)
    c = this._cookies[name]
    if (typeof(c) != "String")
      return ""
    return c

  fun cookies(this)
    // Return the internal cookies map directly
    return this._cookies

  fun status(this, code, text)
    this._status = to_number(code)
    if (len(text) > 0)
      this._status_text = text

  fun content_type(this, ct)
    this._content_type = to_string(ct)

  fun header(this, name, value)
    k = to_string(name)
    v = to_string(value)
    this._headers[k] = v
    // Track in ordered list for emission
    push(this._header_list, [k, v])

  fun redirect(this, location, code)
    c = to_number(code)
    if (c == 0)
      c = 302
    if (c == 301)
      this.status(c, "Moved Permanently")
    else
      this.status(c, "Found")
    this.header("Location", to_string(location))

  fun header_str(this)
    out = "Status: " + to_string(this._status) + " " + this._status_text + "\r\n"
    out = out + "Content-Type: " + this._content_type + "\r\n"
    // Emit headers from the ordered list
    i = 0
    n = len(this._header_list)
    while (i < n)
      pair = this._header_list[i]
      if (typeof(pair) == "Array" && len(pair) >= 2)
        out = out + to_string(pair[0]) + ": " + to_string(pair[1]) + "\r\n"
      i = i + 1
    out = out + "\r\n"
    return out

  fun send(this, body)
    print(this.header_str() + to_string(body))

  fun escape_html(this, s)
    a = to_string(s)
    a = str_replace_all(a, "&", "&amp;")
    a = str_replace_all(a, "<", "&lt;")
    a = str_replace_all(a, ">", "&gt;")
    a = str_replace_all(a, "\"", "&quot;")
    a = str_replace_all(a, "'", "&#39;")
    return a

  // Translate CGI output (headers + body) into a full HTTP/1.1 response string
  // Input: raw string as emitted by a CGI script (e.g., via CGI.send()), containing
  // CGI headers followed by an empty line and then the body.
  // Output: a single HTTP/1.1 response string ready to be written to a socket.
  fun cgi_to_http_response(this, raw)
    out = to_string(raw)
    // Find header/body separator
    sep = find(out, "\r\n\r\n")
    seplen = 4
    if (sep < 0)
      sep = find(out, "\n\n")
      seplen = 2
    if (sep < 0)
      // No CGI headers, treat whole as body
      b = out
      resp = "HTTP/1.1 200 OK\r\n"
      resp = resp + "Content-Type: text/html; charset=utf-8\r\n"
      resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
      resp = resp + "Connection: close\r\n\r\n" + b
      return resp

    header_str = substr(out, 0, sep)
    body = substr(out, sep + seplen, len(out) - sep - seplen)

    // Parse headers
    lines = str_split(header_str, "\n")
    code = 200
    text = "OK"
    // Accumulate headers (excluding Status)
    hh = []  // array of [k, v]
    i = 0
    n = len(lines)
    while (i < n)
      ln = str_trim(lines[i])
      if (len(ln) > 0)
        colon = find(ln, ":")
        if (colon > 0)
          k = str_trim(substr(ln, 0, colon))
          v = str_trim(substr(ln, colon + 1, len(ln) - colon - 1))
          if (str_to_upper(k) == "STATUS")
            // Expect like: 200 OK
            sp = find(v, " ")
            if (sp > 0)
              code = to_number(substr(v, 0, sp))
              text = str_trim(substr(v, sp + 1, len(v) - sp - 1))
            else
              code = to_number(v)
              if (code == 0) code = 200
              text = "OK"
          else
            // Keep other headers
            push(hh, [k, v])
      i = i + 1

    // Build HTTP response
    b = to_string(body)
    resp = "HTTP/1.1 " + to_string(code) + " " + text + "\r\n"
    // Emit collected headers
    j = 0
    m = len(hh)
    has_len = false
    while (j < m)
      p = hh[j]
      if (typeof(p) == "Array" && len(p) >= 2)
        hk = to_string(p[0])
        hv = to_string(p[1])
        if (str_to_upper(hk) == "CONTENT-LENGTH")
          has_len = true
        resp = resp + hk + ": " + hv + "\r\n"
      j = j + 1
    if (!has_len)
      resp = resp + "Content-Length: " + to_string(len(b)) + "\r\n"
    resp = resp + "Connection: close\r\n\r\n" + b
    return resp

  fun url_decode(this, s)
    src = to_string(s)
    out = []
    i = 0
    n = len(src)
    while (i < n)
      ch = substr(src, i, 1)
      if (ch == "+")
        push(out, " ")
        i = i + 1
      else if (ch == "%" && i + 2 < n)
        h1 = substr(src, i + 1, 1)
        h2 = substr(src, i + 2, 1)
        hexdigits = "0123456789ABCDEF"
        v1 = find(hexdigits, str_to_upper(h1))
        v2 = find(hexdigits, str_to_upper(h2))
        if (v1 >= 0 && v2 >= 0)
          code = v1 * 16 + v2
          ascii = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"
          if (code >= 32 && code <= 126)
            push(out, substr(ascii, code - 32, 1))
          else
            push(out, "%")
            push(out, h1)
            push(out, h2)
          i = i + 3
        else
          push(out, ch)
          i = i + 1
      else
        push(out, ch)
        i = i + 1
    return join(out, "")

  fun _merge_params(this, pairs)
    // pairs: array of [key, value] entries
    if (typeof(pairs) != "Array")
      return 0
    i = 0
    n = len(pairs)
    while (i < n)
      p = pairs[i]
      if (typeof(p) == "Array" && len(p) >= 2)
        key = to_string(p[0])
        val = to_string(p[1])
        a = this._params[key]
        if (typeof(a) != "Array")
          this._params[key] = []
        push(this._params[key], val)
      i = i + 1
    return 1

  fun _parse_urlencoded(this, s)
    out = []   // array of [key, value]
    src = to_string(s)
    if (len(src) == 0)
      return out
    // Manually scan so we can treat both '&' and ';' as pair separators (robust across environments)
    pairs = []
    buf = []
    number i = 0
    number n = len(src)
    while (i < n)
      ch = substr(src, i, 1)
      if (ch == "&" || ch == ";")
        push(pairs, join(buf, ""))
        buf = []
      else
        push(buf, ch)
      i = i + 1
    // tail
    push(pairs, join(buf, ""))

    j = 0
    lp = len(pairs)
    while (j < lp)
      kv = pairs[j]
      if (typeof(kv) == "String")
        token = kv
        // Trim CR/LF that might trail if body ended with a newline
        // Reuse str_trim which trims spaces and CR/LF
        token = str_trim(token)
        if (len(token) > 0)
          eq = find(token, "=")
          if (eq >= 0)
            k = substr(token, 0, eq)
            v = substr(token, eq + 1, len(token) - eq - 1)
          else
            k = token
            v = ""
          key = this.url_decode(k)
          val = this.url_decode(v)
          push(out, [key, val])
      j = j + 1
    return out
  
  fun _parse_cookies(this, cookie_str)
    out = {}
    if (len(cookie_str) == 0)
      return out
    semi = str_split(cookie_str, ";")
    i = 0
    while (i < len(semi))
      part = str_trim(semi[i])
      if (len(part) > 0)
        eq = find(part, "=")
        if (eq >= 0)
          k = str_trim(substr(part, 0, eq))
          v = str_trim(substr(part, eq + 1, len(part) - eq - 1))
          out[k] = v
      i = i + 1
    return out
