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

import std/[strutils, re, typetraits, times, tables]
import valido/[email, password]
import "form", "global"

var msgString{.threadvar.}: string

proc vSize(sizeName: string, sizeValue: int, inputName: string, inputData: string, inputType: string) =
  case sizeName:
  of "min":    
    #how we determine size depends on input type.
    case inputType:
    of "string":
      if not (inputData.len >= sizeValue):
        msgString = "The '" & inputName & "' field must have a character count of at least " & $sizeValue & " ."
        addFormError(inputName, msgString)
    of "integer":
      if not (parseIntIf(inputData) >= sizeValue):
        msgString = "The '" & inputName & "' field must have a value of at least " & $sizeValue & " ."
        addFormError(inputName, msgString)
    of "float":
      if not (parseFloat(inputData) >= sizeValue.float):
        msgString = "The '" & inputName & "' field must have a value of at least " & $sizeValue & " ."
        addFormError(inputName, msgString)
  of "max":
    #how we determine size depends on input type.
    case inputType:
    of "string":
      if inputData.len > sizeValue:
        msgString = "The '" & inputName & "' field must have a character count that is no greater than " & $sizeValue & " ."
        addFormError(inputName, msgString)
    of "integer":
      if parseIntIf(inputData) > sizeValue:
        msgString = "The '" & inputName & "' field must have a value that is no greater than " & $sizeValue & " ."
        addFormError(inputName, msgString)
    of "float":
      if parseFloat(inputData) > sizeValue.float:
        msgString = "The '" & inputName & "' field must have a value that is no greater than " & $sizeValue & " ."
        addFormError(inputName, msgString)

proc vType(inputName: string, inputData: string, validators: seq[string]): string =
  var inputType: string
  if "integer" in validators:
    try:
      #don't try to parse empty string as int.
      if inputData.len > 0:
        discard parseIntIf(inputData)
        inputType = "integer" 
    except:
      msgString = "The '" & inputName & "' field must be a whole number."
      addFormError(inputName, msgString)
  elif "float" in validators:
    try:
      discard parseFloat(inputData)
      inputType = "float"
    except:
      msgString = "The '" & inputName & "' field must be a floating point number; a number with a decimal point. i.e. '1.5'"
      addFormError(inputName, msgString)
  elif "boolean" in validators:
    if (inputData == "true") or not (inputData == "false"):
      msgString = "The '" & inputName & "' field must be interpretable as a boolean. i.e. true, or false."
      addFormError(inputName, msgString)
    inputType = "boolean"
  else:
    inputType = "string"
  return inputType

proc vRegex(inputName: string, inputData: string, inputType: string, validators: seq[string]) =
  #check for regex validators.
  for v in validators:  
    if match(v, re"^(min):([0-9]+)$"):
      #get size's value from the string.
      let vNameSeq = v.split(':')
      let sizeName = vNameSeq[0]
      let sizeValue = parseIntIf(vNameSeq[1])
      vSize(sizeName, sizeValue, inputName, inputData, inputType)    
    if match(v, re"^(max):([0-9]+)$"):
      let vNameSeq = v.split(':')
      let sizeName = vNameSeq[0]
      let sizeValue = parseIntIf(vNameSeq[1])
      vSize(sizeName, sizeValue, inputName, inputData, inputType)
    #ex. matches: "required_without:parent_id"
    if match(v, re"^(required_without):([a-z_]+)$"):
      let vNameSeq = v.split(':')
      #skip 0 index, as that's just the name of the validator.
      let conditionField = vNameSeq[1]
      var conditionFieldInputData{.threadvar.}: string
      conditionFieldInputData = formInput(conditionField)
      if conditionFieldInputData.len == 0:
        if inputData.len == 0:
          msgString = "The '" & inputName & "' field is required when " & conditionField & " isn't set."
          addFormError(inputName, msgString)
    if match(v, re"^(must_unset_with):([a-z_]+)$"):
      let vNameSeq = v.split(':')
      #skip 0 index, as that's just the name of the validator.
      let conditionField = vNameSeq[1]
      var conditionFieldInputData: string
      conditionFieldInputData = formInput(conditionField)
      if conditionFieldInputData.len > 0:
        if inputData.len > 0:
          msgString = "The '" & inputName & "' field must be empty if " & conditionField & " is not."
          addFormError(inputName, msgString)
    if match(v, re"^(required_with):([a-z_]+)$"):
      let vNameSeq = v.split(':')
      #skip 0 index, as that's just the name of the validator.
      let conditionField = vNameSeq[1]
      var conditionFieldInputData: string
      conditionFieldInputData = formInput(conditionField)
      if conditionFieldInputData.len > 0:
        if inputData.len == 0:
          msgString = "The '" & inputName & "' field is also required when " & conditionField & " is set."
          addFormError(inputName, msgString)
    if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+)$"):
      let vNameSeq = v.split(':')
      #skip 0 index, as that's just the name of the validator.
      let conditionField1 = vNameSeq[1]
      var conditionField1InputData: string
      conditionField1InputData = formInput(conditionField1)
      let conditionField2 = vNameSeq[3]
      var conditionField2InputData: string
      conditionField2InputData = formInput(conditionField2)
      if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0:
        if inputData.len == 0:
          msgString = "The '" & inputName & "' field is required when " & conditionField1 & " is set and " & conditionField2 & " isn't set."
          addFormError(inputName, msgString)          
    if match(v, re"^(required_with):([a-z_]+):(without):([a-z_]+):(and_without):([a-z_]+)$"):
      let vNameSeq = v.split(':')
      #skip 0 index, as that's just the name of the validator.
      let conditionField1 = vNameSeq[1]
      var conditionField1InputData: string
      conditionField1InputData = formInput(conditionField1)
      let conditionField2 = vNameSeq[3]
      var conditionField2InputData: string
      conditionField2InputData = formInput(conditionField2)
      let conditionField3 = vNameSeq[5]
      var conditionField3InputData: string
      conditionField3InputData = formInput(conditionField3)
      if conditionField1InputData.len > 0 and not conditionField2InputData.len > 0 and not conditionField3InputData.len > 0:
        if inputData.len == 0:
          msgString = "The '" & inputName & "' field is required when " & conditionField1 & " is set and " & conditionField2 & " and " & conditionField3 & " aren't set."
          addFormError(inputName, msgString)
    if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+)$"):
      let vNameSeq = v.split(':')
      #skip 0 index, as that's just the name of the validator.
      let conditionField1 = vNameSeq[1]
      var conditionField1InputData: string
      conditionField1InputData = formInput(conditionField1)
      let conditionField2 = vNameSeq[3]
      if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2:
        if inputData.len == 0:
          msgString = "The '" & inputName & "' field is required when " & conditionField1 & " equals " & conditionField2
          addFormError(inputName, msgString)
    if match(v, re"^(required_when):([a-z_]+):(equals):([a-z_]+):(without):([a-z_]+)$"):
      let vNameSeq = v.split(':')
      #skip 0 index, as that's just the name of the validator.
      let conditionField1 = vNameSeq[1]
      var conditionField1InputData: string
      conditionField1InputData = formInput(conditionField1)
      let conditionField2 = vNameSeq[3]
      let conditionField3 = vNameSeq[5]
      var conditionField3InputData: string
      conditionField3InputData = formInput(conditionField3)
      if conditionField1InputData.len > 0 and conditionField1InputData == conditionField2 and conditionField3InputData.len == 0:
        if inputData.len == 0:
          msgString = "The '" & inputName & "' field is required when " & conditionField1 & " equals " & conditionField2 & " and " & conditionField3 & " is not set."
          addFormError(inputName, msgString)

proc vStandard(inputName: string, inputData: string, inputType: string, validators: seq[string]) =
  if "email" in validators:
    if not isEmail(inputData):
      msgString = "The '" & inputName & "' field is not recognized as a valid email address."
      addFormError(inputName, msgString)
  if "min_complexity" in validators:
    if not isStrongPassword(inputData):
      msgString = "The '" & inputName & "' field is not random/complex enough. Try to make your " & inputName & " more unpredictable by adding random numbers, random letter casing, unrelated words, special characters, etc."
      addFormError(inputName, msgString)
  
proc vHardcoded*(inputDataAll: Table[string, string], validators: seq[string]) =
  ##Warning: this validator requires specific (hardcoded) input names to exist in the posted form data.
  if "future_datetime" in validators:
    let nowDT = now()
    var inputDT: DateTime
    let inputDateString = inputDataAll.getOrDefault("send_date", "")
    let inputTimeHrString = inputDataAll.getOrDefault("send_time_hr", "")
    let inputTimeMinString = inputDataAll.getOrDefault("send_time_min", "")
    let inputTimeAmPmString = inputDataAll.getOrDefault("send_time_am_pm", "")
    if inputDateString.len > 0 and inputTimeHrString.len > 0 and inputTimeMinString.len > 0 and inputTimeAmPmString.len > 0:
      let inputDatetimeString = inputDateString & " " & inputTimeHrString & ":" & inputTimeMinString & ":" & inputTimeAmPmString
      inputDT = parse(inputDatetimeString, "yyyy-M-d h:m:tt")
    else:
      inputDT = now()
    if inputDT <= nowDT:
      msgString = "The 'send_date' and 'send_time' fields combined must be a DateTime in the future (compared to the DateTime at form submit)."
      addFormError("send_date", msgString)

proc vInput*(inputName: string, validators: seq[string]) =
  var inputType: string
  var inputData: string
  var inputDataAll: Table[string, string]
  inputData = formInput(inputName) 
  inputDataAll = formInputAll()
  #check if 'required' is set and validate input if it exists. otherwise return error. 
  if "required" in validators:
    if (inputData.len == 0):
      msgString = "The '" & inputName & "' field is required."
      addFormError(inputName, msgString)
    else:
      #proceed with validation.
      #get inputType.
      inputType = vType(inputName, inputData, validators)
      #check for other validators and run if they exist.
      vStandard(inputName, inputData, inputType, validators)
      vHardcoded(inputDataAll, validators)
      vRegex(inputName, inputData, inputType, validators)
  #required is not set, but input could still exist. validate it.
  else:
    #get inputType.
    inputType = vType(inputName, inputData, validators)
    #check for other validators and run if they exist.
    vStandard(inputName, inputData, inputType, validators)    
    vHardcoded(inputDataAll, validators)
    vRegex(inputName, inputData, inputType, validators)
