#[Copyright 2025 ITwrx.
This file is part of Simple Site Manager.
Simple Site Manager is released under the GNU Affero General Public License 3.0.
See COPYING or <https://www.gnu.org/licenses/> for details.]#

import std/[cgi, strtabs, strutils, cookies, uri, tables] 
import jsony, guildenstern/httpserver, sqliteral
import "auth", "global"

type
  FormError* = object
    fieldName*, fieldMessage*: string
type
  FormOldInput* = object
    fieldName*, fieldOldInput*: string
type
  FormResult* = object
    id*: int
    message*, messageClass*, errors*, oldInputs*: string

var formError*: FormError
var formErrors*: seq[FormError]
var formOldInput*: FormOldInput
var formOldInputs*: seq[FormOldInput]
var formResult*: FormResult

#might still be in use by cookie FR type. too lazy to investigate now.
proc clearFormResult*() =
  formResult = FormResult(message : "",
    messageClass : "",
    errors : "",
    oldInputs : "")

proc assignErrorFR*(formErrors: seq[FormError], formOldInputs: seq[FormOldInput]): FormResult =
  {.gcsafe.}:
    formResult.message = "Error(s) encountered while validating form input(s)."
    formResult.messageClass = "form-error"
    formResult.errors = toJson(formErrors)
    formResult.oldInputs = toJson(formOldInputs)
  
proc assignGeneralErrorFR*(errorMsg: string): FormResult =
  {.gcsafe.}:
    formResult.message = errorMsg
    formResult.messageClass = "form-error"
    
proc assignGeneralSuccessFR*(successMsg: string): FormResult =
  {.gcsafe.}:
    formResult.message = successMsg
    formResult.messageClass = "form-success"

proc assignLoginSuccessFR*(): FormResult =
  {.gcsafe.}:
    formResult.message = "You have logged in successfully."
    formResult.messageClass = "form-success"
  
proc assignCECreateSuccessFR*(): FormResult =
  {.gcsafe.}:
    formResult.message = "Content Entity successfully created."
    formResult.messageClass = "form-success"
  
proc assignCEEditSuccessFR*(): FormResult =
  {.gcsafe.}:
    formResult.message = "Content Entity successfully edited."
    formResult.messageClass = "form-success"
  
proc assignCEDeleteSuccessFR*(): FormResult =
  {.gcsafe.}:
    formResult.message = "Content Entity successfully deleted."
    formResult.messageClass = "form-success"
    
#using cookie to store login page formResult, because there's no session to use as formResult id yet.  
proc getCookieFormResult*(): FormResult =  
  let cookieString = http.headers.getOrDefault("cookie")
  let cookiesTable = parseCookies(cookieString)
  var frJsonString: string
  if cookiesTable.hasKey("form_result"):
    frJsonString = cookiesTable["form_result"]
  var formResult: FormResult
  if frJsonString.len > 0:
    formResult = frJsonString.fromJson(FormResult)
    formErrors = @[]
    clearFormResult()
  if not formResult.message.len > 0:             
    formResult.id = 0
    formResult.message = ""
    formResult.messageClass = ""
    formResult.errors = ""
    formResult.oldInputs = ""
  return formResult

#using strtabs for storing all other formResults.
proc getFormResult*(): FormResult =
  let sessionId = getSessionIdFromCookies()
  var frJsonString, strTabSessionId: string
  if frStrTab.hasKey("sessionId"):
    strTabSessionId = frStrTab["sessionId"]
    if strTabSessionId == sessionId:
      if frStrTab.hasKey("frJson"):
        frJsonString = frStrTab["frJson"]
        var newFormResult: FormResult
        if frJsonString.len > 0:
          newFormResult = frJsonString.fromJson(FormResult)
          #reset formErrors seq variable used in form handlers.
          formErrors = @[]
          #clear the existing formResult so it won't show on next page's GET (without new POST).
          clear(frStrTab, modeCaseSensitive)
        if not newFormResult.message.len > 0:             
          newFormResult.id = 0
          newFormResult.message = ""
          newFormResult.messageClass = ""
          newFormResult.errors = ""
          newFormResult.oldInputs = ""
        return newFormResult
      
proc setFR*() =
  let sessionId = getSessionIdFromCookies()
  let frJson = formResult.toJson()
  frStrTab = {"sessionId": sessionId, "frJson": frJson}.newStringTable  

proc formInput*(input: string): string =
  {.gcsafe.}:
    if server.contenttype == Compact:
      #readData:cgi, getBody:Guildenstern, getOrDefault:strtabs. 
      return readData(getBody()).getOrDefault(input)
    else:
      #getMPStringInput(input: string)
      echo "not url-encoded"
      
proc formInputAll*(): Table[string, string] =
  {.gcsafe.}:
    if server.contenttype == Compact:
      #getBody:Guildenstern
      let formDataStr = getBody()          
      var formData = initTable[string, string]()  
      # Use decodeQuery from the uri module
      for (key, value) in decodeQuery(formDataStr):
        formData[key] = value
      return formData
    else:
      echo "not url-encoded"

proc formInputInt*(input: string): int =
  {.gcsafe.}:  
    let readInput = readData(getBody()).getOrDefault(input)
    return parseIntIf(readInput)

template formInputSeq*(input: string): seq[string] =
  {.gcsafe.}:
    readData(getBody()).getOrDefault(input)

proc addFormError*(inputName: string, msgString: string) =
  {.gcsafe.}:
    formError.fieldName = inputName
    formError.fieldMessage = msgString
    formErrors.add(formError)

proc addFormOldInput*(inputName: string, inputData: string) =
  {.gcsafe.}:
    formOldInput.fieldName = inputName
    formOldInput.fieldOldInput = inputData
    formOldInputs.add(formOldInput)
    
#takes a fieldName and returns the fieldMessage.
proc fFieldMsg*(fr: FormResult, fieldName: string): string =
  {.gcsafe.}:
    if fr.errors.len() > 0:      
      let errors = fromJson(fr.errors, seq[FormError])    
      for error in errors:
        if error.fieldName == fieldName:
          return error.fieldMessage

#this may need to handle more than one message per form field at some point, but not for this app (yet).
proc fErrorMsg*(fr: FormResult, fieldName:string): string =
  {.gcsafe.}:
    if fFieldMsg(fr, fieldName).len > 0:
      return """<span class="text-red-500">""" & fFieldMsg(fr, fieldName) & "</span><br>"

#takes a fieldName and returns the fieldOldInput.
proc fOldInput*(fr: FormResult, fieldName: string, defaultValue = ""): string =
  {.gcsafe.}:
    var fieldOldInput: string
    if fr.oldInputs.len() > 0:
      let oldInputs = fromJson(fr.oldInputs, seq[FormOldInput])   
      for oldInput in oldInputs:
        if oldInput.fieldName == fieldName:
          fieldOldInput = oldInput.fieldOldInput
      return fieldOldInput
    else:
      return defaultValue
    
proc getOldInputJson*(): string =
  {.gcsafe.}:
    let postData = readData(getBody())
    #pairs is a strtabs iterator.
    for key,value in pairs(postData):
      if key.len() > 0:
        if key != "password" and key != "csrf_token":
          addFormOldInput(key, value)
    return toJson(formOldInputs)

#the empty string defaultValue makes it possible to optionally supply a value from the DB: as needed in edit forms. 
#checked proc works on radios and checboxes.
proc checked*(fr: FormResult, groupName: string, targetValue: string, defaultValue = ""): string =
  {.gcsafe.}:
    if fOldInput(fr, groupName, defaultValue) == targetValue:
      return "checked"

proc selected*(fr: FormResult, selectName: string, targetValue: string, defaultValue = ""): string =
  {.gcsafe.}:
    if fOldInput(fr, selectName, defaultValue) == targetValue:
      return "selected='selected'"
