-- rest.lua
-- if not util.from4d() then
local util = require "util"
local json = require "json"
local date = require "dt"
local auth = require "auth"
local db = require "database"
local zlib = require "zlib"
local lz4 = require "lz4"
local l = require"lang".l
local peg = require "peg"
local fn = require "fn"
local dt = require "dt"
local http = require "http"
local dredir2 = require "db/db_redirect2"

local option = util.prf("system/option.json")
local stopCollectgarbage = option.stop_collectgarbage
if stopCollectgarbage == nil then
	stopCollectgarbage = true
end
if stopCollectgarbage then
	print('rest.lua / collectgarbage("stop")')
	collectgarbage("stop")
	-- else
	-- print('rest.lua / use collectgarbage')
end
local useCompressDebug = option.compress_debug or false
-- end
local srv

local authList = {}
authList["cHJvdGFjb246cmVzdFdoYzEuMA=="] = "/rest/warehouse-control/*" -- protacon:restW...
authList["bWFuYWdlYXBwOndlYm1hbmFnZXJpMy0="] = { -- manageapp:webm..3-
	"/rest/ma/timer*",
	"/rest/ma/gitupdate",
	"/rest/ma/distwin",
	"/rest/ma/distmacwin",
	"/rest/ma/restart",
	"/rest/ma/cache/clear",
	"/rest/ma/ping",
	"/rest/ma/query/data"
}

local tcpCallCount = 0
local prevTcpCallCount = -1
local tcpPollCount = 0
local disconnect = false
local maxSecondsBeforeClose = 10 * 60 -- 10 minutes
local minCompressSize = 1000 -- 1400 -- MUST match IN rest.lua AND database-4drest.lua
local answerCount = 0
local answerSkip = 0 -- tonumber(arg[1]) or 0
local pollAfterSkip
local debugPrintChars = 1500
local debugLevel = 0
local sqlTxt = ""
local answerTxt = ""
local sockets = {}
local calls = {}
local databases = {}
local printIncompleteRequest = util.prf("system/option.json").print_incomplete_request or true

if util.from4d() then
	answerSkip = 150000
	pollAfterSkip = 8 -- 25 -- 90000 -- how many poll()+yield() loops until server poll loop calls pollAfter()
else
	answerSkip = 150000
	pollAfterSkip = 750 -- 50000 --answerSkip
end

local function setDebugLevel(level)
	debugLevel = level
end

-- rest_answerOk and rest_answerError must be declared before plg.setCallbacks() -call
local function answerOk(info)
	if info then
		return {status = "ok", info = info}
	end
	return {status = "ok"}
end

local function answerError(txt)
	if type(txt) == "table" then
		if txt.error then
			return nil, json.toJsonRaw(txt)
		end
		txt = json.toJsonRaw(txt)
	end
	return nil, txt -- '{"error": "'..txt..'"}'
end

local function requestType(socket)
	local s, e = socket.request:find("GET ", 1, true)
	local requestType
	if s then
		requestType = "GET"
	else
		s, e = socket.request:find("POST ", 1, true)
		if s then
			requestType = "POST"
		else
			s, e = socket.request:find("OPTIONS ", 1, true) and socket.request:find("Access-Control-Request-Method: POST", 1, true)
			if s then
				requestType = "POST"
			else
				s, e = socket.request:find("PUT ", 1, true)
				if s then
					requestType = "PUT"
				else
					s, e = socket.request:find("DELETE ", 1, true)
					if s then
						requestType = "DELETE"
					end
				end
			end
		end
	end
	return requestType, e or 1
end

local function parseHeaderField(txt, tag, start)
	local ret -- will return nil if not found
	local s, e = txt:find(tag, start, true)
	if not s then
		s, e = txt:lower():find(tag:lower(), start, true)
	end
	if s then
		local s2 = txt:find("\r\n", e + 1, true)
		if s2 then
			ret = txt:sub(e + 1, s2 - 1)
		end
	end
	return ret
end

local function addToBody(socket)
	-- local requestLen = #socket.request
	local neededLen = #socket.header + 4 + socket.bodyLength -- +4 = header/body separator \r\n\r\n
	if #socket.request < neededLen then
		-- got less data than needed, this is ok
		return false
	end
	local start = #socket.header + 5 -- 4 + 1
	socket.body = socket.request:sub(start, start + socket.bodyLength - 1)
	if #socket.request > neededLen then
		-- got more data than needed, save for next socket.request in this socket
		socket.request = socket.body:sub(neededLen + 1)
		return true -- do not delete
	end
	-- got just what we needed
	socket.delete = true -- close and delete after answer
	return true
end

-- local httpStartErr = "HTTP/1.1 404 Not Found\nAccess-Control-Allow-Origin: *\nServer: masrv/0.1.0\nContent-Type: application/json\nContent-Length: "
-- local httpStartAuth = 'HTTP/1.1 401 Unauthorized\nAccess-Control-Allow-Origin: *\nServer: masrv/0.1.0\nContent-Type: application/json\nWWW-Authenticate: Basic realm="Manage Rest Server"\nContent-Length: '
-- httpStartErr = httpStartErr:gsub("\n", "\r\n")
-- httpStartAuth = httpStartAuth:gsub("\n", "\r\n")

-- local httpStart = "HTTP/1.1 200 OK\nServer: MA-srv/0.3.0\nAccess-Control-Allow-Origin: http://127.0.0.1\nContent-Type: application/json\nContent-Length: "
-- local httpStart = "HTTP/1.1 200 OK\nServer: MA-srv/0.3.0\nAccess-Control-Allow-Origin: *\nAccess-Control-Expose-Headers: Content-Type\nAccess-Control-Allow-Headers: Content-Type\nContent-Type: application/json\nContent-Length: "
local httpStart = "HTTP/1.1 200 OK\nServer: MA-srv/0.3.0\nAccess-Control-Allow-Origin: *\nAccess-Control-Expose-Headers: Content-Length\nAccess-Control-Allow-Headers: Content-Type\nContent-Type: text/plain\nContent-Length: "
-- local httpStart = "HTTP/1.1 200 OK\nServer: MA-srv/0.3.0\nAccess-Control-Allow-Origin: *\nContent-Type: application/json\nContent-Length: "

httpStart = peg.replace(httpStart, "\n", "\r\n")
local answerTbl = {}
answerTbl[1] = httpStart -- httpStartAuth -- will contain socket.body length later
answerTbl[2] = "" -- will contain socket.body length later
answerTbl[3] = "" -- will contain extra headers
answerTbl[4] = "\r\n\r\n"

local function reconnect()
	if disconnect then
		for key, c in pairs(databases) do
			if not c.conn then
				db.reconnect(c)
				if not c.conn then
					util.printWarning(l "*** Reconnect[" .. key .. "] failed, poll count: " .. tcpPollCount)
				end
			end
		end
	end
end

local prevErrorText
local function createAnswerData(socket, dataParam)
	local data
	if type(dataParam) == "table" then
		if dataParam.info and dataParam.info.error then
			if dataParam.error == nil then
				dataParam.error = {}
			end
			if dataParam.error ~= dataParam.info.error then
				if type(dataParam.error) == "string" then
					dataParam.error = {dataParam.error}
				end
				dataParam.error = util.addError(dataParam.error, dataParam.info.error)
			end
			dataParam.info.error = nil
			answerTxt = dataParam.error
		end

		if false and dataParam.info and dataParam.data then
			-- force error and info part as first in return
			if dataParam.error then
				answerTxt = json.toJsonRaw(dataParam.error)
				data = '{"error":' .. answerTxt .. ',\n"info":' .. json.toJsonRaw(dataParam.info) .. ',\n"data":' .. json.toJsonRaw(dataParam.data) .. '}'
			else
				answerTxt = json.toJsonRaw(dataParam.info)
				data = '{"info":' .. answerTxt .. ',\n"data":' .. json.toJsonRaw(dataParam.data) .. '}'
			end
		elseif not dataParam.http_header then
			local err
			data, err = json.toJsonRaw(dataParam)
			if type(data) ~= "string" then
				if type(err) == "string" then
					answerTxt = l("error in data convert to json: '%s'", err:sub(1, 500))
				else
					answerTxt = l("error in data convert to json")
				end
				answerTxt = "  *** " .. answerTxt:gsub("error", "err") -- if answerTxt starts wirh "err" or contains "error" then 4d worker will stop!!!
				util.printWarning(answerTxt)
			else
				answerTxt = "" -- data:sub(1, 200) -- slow, set only when we are going to print this
			end
		end
		-- dataParam = nil
		-- answerTbl[1] = httpStart
		--[[elseif dataParam == "auth" then
		answerTbl[1] = httpStartAuth
		data = ""]]
	elseif dataParam then
		-- answerTbl[1] = httpStartErr
		data = dataParam
		local err = tostring(data)
		if err ~= prevErrorText then -- optimize performance tests
			prevErrorText = err
			util.printWarning("  *** Rest call error: " .. err, "print-call-path")
		end
	else
		util.printError("worker call did not return any data parameter")
		-- answerTbl[1] = httpStartErr
		data = ""
	end
	if data == nil then
		data = ""
	elseif type(data) ~= "string" then
		data = tostring(data)
	end
	local bodyLen = #data
	local doCompress = bodyLen >= minCompressSize
	-- print("do zlib.gzip: "..tostring(doCompress), bodyLen)
	if doCompress and socket.compress == "gzip" then
		-- print("zlib.gzip: "..#data)
		answerTbl[3] = "\r\nContent-Encoding: gzip\r\nContent-Uncompressed-Length: " .. bodyLen -- extra headers
		data, bodyLen = zlib.gzip(data)
	elseif doCompress and socket.compress == "lz4" then -- => always compress
		local err
		local uncompressedSize = #data
		answerTbl[3] = "\r\nContent-Encoding: lz4\r\nContent-Uncompressed-Length: " .. uncompressedSize -- extra headers
		if useCompressDebug then
			local time = util.microSeconds()
			data, err = lz4.compress(data)
			time = util.microSeconds(time)
			bodyLen = data and #data or 0
			print(l("lz4.compress: %d / %d = %d%%, time %d µs", bodyLen, uncompressedSize, math.floor(bodyLen / uncompressedSize * 100), time))
		else
			data, err = lz4.compress(data)
			bodyLen = data and #data or 0
		end
		if err then
			util.printError(l("lz4.compress failed with error '%s'", err))
		end
	elseif dataParam and dataParam.http_header then
		answerTbl[3] = "\r\n" .. dataParam.http_header
		-- extra headers like Connection: upgrade -answer
		-- websocket, tls, http2
	else
		answerTbl[3] = "" -- no extra headers
	end
	if dataParam and dataParam.http_status then
		-- answerTbl[1] = peg.replace(answerTbl[1], "Content-Type: application/json\r\n", "")
		answerTbl[1] = peg.replace(answerTbl[1], "200 OK", dataParam.http_status)
	end
	answerTbl[2] = tostring(bodyLen) -- must be after possible gzip
	local ret = table.concat(answerTbl) .. data
	return ret
end

local function basicAuth(socket)
	if authList[socket.auth] then
		if type(authList[socket.auth]) == "string" then
			if authList[socket.auth] == socket.uri then
				return true
			end
			if authList[socket.auth]:sub(-1) == "*" then
				if peg.find(socket.uri, authList[socket.auth]:sub(1, -2)) > 0 then
					return true
				end
			end
		elseif type(authList[socket.auth]) == "table" then
			local ret = false
			fn.each(function(uri)
				if ret == true then
					-- exit loop
				elseif uri == socket.uri then
					ret = true
				elseif uri:sub(-1) == "*" and peg.find(socket.uri, uri:sub(1, -2)) > 0 then
					ret = true
				end
			end, authList[socket.auth])
			return ret
		end
	end
	return false
end

local function uncompress(data, compression)
	local ret, err
	if compression == "gzip" then
		ret, err = zlib.uncompress(data)
	elseif compression == "lz4" then
		ret, err = lz4.decompress(data)
	else
		err = l("unsupported compression '%s'", tostring(compression))
	end
	return ret, err
end

local function jsonValue(socket, answer, uri)
	local tag = answer.param
	local func = answer.func
	local jsonData = socket.body
	if not func then
		return answerError(l "callback function is nil")
	elseif not jsonData then
		return answerError(l "json missing")
	elseif type(jsonData) == "string" and #jsonData < 2 then
		return answerError(l "json length is less than 2")
	end
	local param, ret, err
	if type(jsonData) == "table" then
		param = jsonData.param
	else
		if jsonData and #jsonData >= 2 then
			if #jsonData ~= socket.bodyLength then
				util.printError(l("call body length '%s' is not same as header content-length '%s'", util.formatInt(#jsonData), util.formatInt(socket.bodyLength)))
			end
			if socket.uncompress then
				if false and util.isWin() then
					util.writeFile("http_gzip_call.txt", socket.header .. "\r\n\r\n" .. jsonData)
				end
				jsonData, err = uncompress(jsonData, socket.uncompress)
			end
			if jsonData then -- uncompress may return nil
				if false and util.isWin() then
					util.writeFile("http_call.txt", jsonData)
				end
				param, err = json.fromJson(jsonData)
			end
		else
			err = l "json size is less than 2"
		end
		if err then
			ret, err = answerError(l "invalid json: " .. err)
		end
	end
	if not err then
		if tag and not param[tag] then
			ret, err = answerError(l "json tag is missing, tag: " .. tag)
		else
			local authValid, authTable
			if socket.auth then
				authValid = basicAuth(socket)
				authTable = socket.auth
			end
			if not authValid then
				authValid, authTable = auth.authenticate(socket.uri, param)
			end
			if authValid ~= true then
				return authTable
			else
				if authTable then
					param.auth = authTable
				end
				if tag then
					local cleanedParam = json.fixNull(param[tag], socket.uri)
					ret, err = func(cleanedParam) -- call function
				else
					local cleanedParam = json.fixNull(param, socket.uri)
					ret, err = func(cleanedParam) -- call real worker function
				end
				if param.sql then
					sqlTxt = param.sql
				end
			end
		end
	end

	if ret ~= nil and type(ret) ~= "table" then
		util.printError(" ** REST RETURN ERROR: return value is not a table, uri: " .. tostring(uri))
		if err == nil then
			err = tostring(ret)
		end
		ret = nil
	end
	--[[
	if type(ret) == "table" and err then
		print(" ** REST ERROR: "..err)
		if not ret.info then
			ret.info = {}
		end
		if not ret.info.error then
			ret.info.error = tostring(err)
		end
	elseif ret == nil and err then
	]]
	if ret == nil and err then
		util.printWarning(" ** REST ERROR: " .. err, "print-call-path")
		ret = {info = {error = tostring(err)}}
		-- param.auth = param.auth -- ret.auth = param.auth
	elseif ret == nil then
		util.printError(" ** REST ERROR: return is nil without error, uri: " .. tostring(uri))
		ret = {info = {error = " ** REST ERROR: return is nil without error"}}
		-- param.auth = param.auth -- ret.auth = param.auth
	else
		if ret.no_auth_return then
			ret = ret.data
		elseif not ret.auth then
			if type(param.auth) == "table" then
				ret.auth = param.auth
			end
		end

		local function fixGridWidth(grids)
			for _, grid in pairs(grids) do
				if grid.columns and #grid.columns > 0 and not grid.columns[1].id then
					for i, col in ipairs(grid.columns) do
						col.id = i
						col.width = util.round((col.width or 0) * 1.56, 0) -- TODO: fix somewhere else
						if col.type == nil and col.field ~= nil then
							-- if col.field == nil then
							if not col.variable and db.fieldNameToNumber(col.field) == nil and db.fieldNameToNumber(dredir2.externalName(col.field)) == nil then
								util.printWarning(l("grid column '%s' is not a valid field", col.field))
							else
								col.type = db.fieldTypePrefix(col.field)
							end
						end
					end
				end
			end
		end

		if ret.link_grid then
			if type(ret.link_grid) ~= "table" then
				util.printError(l "ret.link_grid type is not a table")
			else
				fixGridWidth({ret.link_grid})
			end
		end

		if ret.grid then
			if type(ret.grid) ~= "table" then
				util.printError(l "ret.grid type is not a table")
			else
				fixGridWidth(ret.grid)
				for _, grid in pairs(ret.grid) do
					if grid.data and #grid.data > 0 and not grid.data[1].id then
						for i, rec in ipairs(grid.data) do
							rec.id = i
						end
					end
				end
			end
		end
	end
	return ret
end

local createTypeAnswer
local function createPostAnswer(socket)
	local data
	local answer = calls[socket.uri]
	if answer and answer.callType == "POST" then
		if socket.type and socket.data then
			data = createTypeAnswer(socket, answer)
		elseif socket.uri == "/rest/ma/soap" then
			data = answer.func(socket)
		else
			data = jsonValue(socket, answer, socket.uri)
		end
	else
		data = l("unknown POST uri: " .. socket.uri)
	end
	return data
end

local function createGetAnswer(socket)
	local data
	local answer = calls[socket.uri]
	if answer and answer.callType == "GET" then
		if socket.type and socket.data then
			data = createTypeAnswer(socket, answer)
		elseif (socket.auth and basicAuth(socket)) then
			data = answer.func(nil) -- socket.uri
		elseif http.headerValueFound(socket.header, "Connection", "Upgrade") then
			data = answer.func(socket) -- will set socket.type = "ws" or something else
		else
			data = l("invalid basic authorization for GET uri '%s'", socket.uri)
		end
	else
		if socket and socket.uri then
			data = l("unknown GET uri '%s'", socket.uri)
		else
			data = l("GET uri does not exist")
		end
	end
	return data
end

createTypeAnswer = function(socket, answer)
	local data, dataType, again
	local dataFunc = answer.func
	data, dataType, again = dataFunc(socket)
	if data == "close" then
		socket.type = nil
		socket.close = true
	else
		socket.data = nil
		socket.request = ""
		local err
		if type(data) == "table" then
			socket.body = data
		else
			socket.body, err = json.fromJson(data)
		end
		if err then
			data = l("websocket body json is invalid, error '%s'", tostring(err))
		else
			answer = calls[socket.body.uri]
			if answer then
				socket.uri = socket.body.uri
				socket.requestType = answer.callType
				if socket.requestType == "GET" then
					data = createGetAnswer(socket) -- recursive call
				elseif socket.requestType == "POST" then
					data = createPostAnswer(socket)
					data.time = dt.currentStringMs()
					data = json.toJsonRaw(data)
					data = dataFunc(socket, data) -- convert to websocket bytedata
					socket.raw_data = true
				else
					data = l("unknown websocket call type (not GET/POST)")
				end
			else
				data = l("unknown websocket call '%s'", socket.body.uri)
			end
		end
	end
	return data
end

local function createAnswer(socket)
	local data
	util.clearError()
	if socket.httpErr then
		data = socket.httpErr
	elseif socket.requestType == "GET" then
		data = createGetAnswer(socket)
	elseif socket.requestType == "POST" then
		data = createPostAnswer(socket)
	elseif socket.requestType == "PUT" then
		data = l("unsupported call type PUT")
	elseif socket.requestType == "DELETE" then
		data = l("unsupported call type DELETE")
	else
		data = l("unknown call type (not GET/POST/PUT/DELETE)")
	end

	if type(data) ~= "table" then
		socket.delete = true -- error -> close and delete after answer
	end
	if data == "close" or socket.raw_data then
		return data
	end
	return createAnswerData(socket, data)
end

local function parseCall(data, socketId, sockets)
	local pos = 1
	-- get previous saved data
	local socket = sockets[socketId]
	if not socket then
		-- create new table  entry for socketId
		sockets[socketId] = {}
		socket = sockets[socketId]
		socket.request = ""
	end
	socket.lastCall = date.currentDateTime()

	if socket.type then
		socket.data = data
		return socket
	end
	-- add to new/previous saved data
	-- if #socket.request > 0 then
	-- util.printWarning("*** add "..#data.." bytes to request, add data:\n'"..data.."'\n")
	-- end
	socket.request = socket.request .. data

	if not socket.requestType then
		socket.requestType, pos = requestType(socket)
		if not socket.requestType then
			return false
		end
		if socketId.tls_ctx then
			socket.protocol = "HTTPS"
		else
			socket.protocol = "HTTP"
		end
	else
		pos = #socket.requestType + 1
	end

	if socket.bodyLength then -- header already parsed, not enough body data
		if not addToBody(socket) then
			return false
		end
	else
		local s2, e2 = socket.request:find(" HTTP/", pos, true) -- HTTP/1.1 or other protocol, not HTTP vs HTTPS
		if not s2 then
			return false
		else
			socket.uri = socket.request:sub(pos + 1, s2 - 1)
			if socket.uri:find("PTIONS ") == 1 then -- OPTIONS can change to POST in cors calls
				socket.uri = socket.uri:sub(8)
			end

			-- full socket.header found?
			local s, e = socket.request:find("\r\n\r\n", e2 + 1, true)
			if not s then
				return false
			else
				-- now we have full socket.header
				if socket.requestType == "GET" then
					socket.bodyLength = 0
					if #socket.request == e then -- no more than one request
						-- faster this way
						socket.header = socket.request
						socket.delete = true -- close and delete after answer
					else
						socket.header = socket.request:sub(1, s - 1)
						addToBody(socket) -- just to set remaining request
					end
				else
					-- post, put, delete
					socket.header = socket.request:sub(1, s - 1)
					socket.bodyLength = parseHeaderField(socket.request, "Content-Length: ", e2 + 1)
					if not socket.bodyLength then
						-- client error, could also assume that we got just what we needed
						socket.httpErr = "411 Length Required"
						return socket -- answer with error
					else
						socket.bodyLength = tonumber(socket.bodyLength)
						if not addToBody(socket) then
							return false
						end
					end
				end
			end
		end
	end

	-- Authorization: Basic cmVzdDphdXRo
	socket.auth = parseHeaderField(socket.header, "Authorization: Basic ", 1)

	-- Accept-Encoding: gzip, deflate
	local acceptEncoding = parseHeaderField(socket.header, "Accept-Encoding: ", 1)
	if acceptEncoding then
		if acceptEncoding:find("gzip", 1, true) then
			socket.compress = "gzip"
		elseif acceptEncoding:find("lz4", 1, true) then
			socket.compress = "lz4"
		end
	end

	local contentEncoding = parseHeaderField(socket.header, "Content-Encoding: ", 1)
	if contentEncoding then
		if contentEncoding:find("gzip", 1, true) then
			socket.uncompress = "gzip"
		elseif contentEncoding:find("lz4", 1, true) then
			socket.uncompress = "lz4"
		end
	end
	return socket
end

local basicAuthPatt = peg.toPattern("Authorization: Basic ") * peg.other(peg.patt.endOfLine, 1) * peg.patt.endOfLine
local function tcpAnswer(data, socketId)
	-- print("tcpAnswer")
	local answer
	local time = util.seconds()
	tcpCallCount = tcpCallCount + 1
	local oldRequest = sockets[socketId] and sockets[socketId].request or ""
	local socket = parseCall(data, socketId, sockets)
	if not socket then
		if printIncompleteRequest and #oldRequest > 0 then
			local oldRequestSub = #oldRequest < 400 and #oldRequest or 400
			local dataSub = #data < 400 and #data or 400
			local str = l("\n*** incomplete request, tcpCallCount: %d, %s,\n  previous request: length %d bytes, data: '\n%s'...\n  new data: length %d bytes, data: '\n%s'...\n***", tcpCallCount, date.currentStringLocal(), #oldRequest, peg.replace(oldRequest:sub(1, oldRequestSub), "\r", ""), #data, peg.replace(data:sub(1, dataSub), "\r", "")) -- ..", queryCount: "..c.sql.queryCount
			util.printWarning(str)
		end
		return nil
	elseif not socket.close then
		answerCount = answerCount + 1
		local printAnswer = answerCount == 1 or util.skip(answerCount, answerSkip) == 0
		local answerTxtStart = ""
		--[[ if util.isMac() then -- for debug
			printAnswer = true
		end]]
		if printAnswer then
			local str = answerCount .. ". uri: " .. socket.protocol .. ", " .. socket.requestType .. " '" .. socket.uri .. "' " .. date.toString(date.currentDateTime()) .. "\n'" .. peg.replace(socket.request:sub(1, 900), basicAuthPatt, "Authorization: Basic ???\r\n") .. "'"
			-- ..", queryCount: "..c.sql.queryCount.."\n'"..socket.request.."'"
			print("\n" .. peg.replace(str, "\r\n", "\n"))
		else
			answerTxtStart = " " .. answerCount .. ". uri: " .. tostring(socket.requestType) .. " '" .. tostring(socket.uri) .. "' "
		end
		reconnect()
		answer = createAnswer(socket)
		if printAnswer then
			time = util.seconds(time)
			print(answerTxtStart .. "\n " .. answerCount .. ". answer time: " .. util.seconds_to_clock(time, 5) .. "\n  " .. answerTxt .. "\n" .. sqlTxt)
			answerTxt = ""
			sqlTxt = ""
			-- print(" answer: "..#answer.." bytes")
			-- sql4d.printUtfTime()
			-- print("\n")
		end
	end
	if socket.delete then -- socket complete
		if socket.type and not socket.close then
			socket.delete = false
		else
			if not answer and not socket.close then -- ok to have nos asnwer with websocket close
				util.printWarning("socket.delete is true, but there is no answer")
			end
			sockets[socketId] = nil
			-- else
			-- need more socket.body
		end
	end
	-- if disconnect then
	-- db.disconnect(c)
	-- end
	-- print("tcpAnswer")
	return answer, socket.close
end

local function tcpClose(sock) -- pollReturnValue
	if sockets[sock] then
		local len = sockets[sock].request and #sockets[sock].request
		if len then
			len = util.format_num(len, 0)
		else
			len = l("(no request)")
		end
		print(l("\n*** tcp close for socket %s, request length: %s bytes ***", sock, len))
		sockets[sock] = nil
	end
end

local function pollAfter(pollReturn)
	if not srv then
		srv = require "server"
	end
	tcpPollCount = tcpPollCount + 1
	if pollReturn == 0 and not util.from4d() then
		srv.collectGarbage(pollReturn, tcpPollCount)
	end
	if debugLevel > 0 and not util.from4d() then
		io.write(tcpPollCount .. " ")
		if tcpPollCount % 20 == 0 then
			io.write("\n")
		end
		io.flush()
	end
	if util.skip(tcpPollCount, pollAfterSkip) == 0 then
		if util.from4d() then
			srv.serverPause()
			-- jit.flush()
		else
			if prevTcpCallCount == tcpCallCount then
				util.printToSameLine(" | poll after: " .. tcpPollCount .. ", " .. dt.currentString())
				-- io.write("| "..dt.currentString().." poll after: "..tcpPollCount)
				-- The ansi code for going to the beginning of the previous line is "\033[F"
				-- print("\033[F".."poll after: "..tcpPollCount..", "..dt.currentString())
			else
				print("\npoll after: " .. tcpPollCount .. ", " .. dt.currentString())
			end
			prevTcpCallCount = tcpCallCount
		end
	end
	local current = date.currentDateTime()
	for socketNum, socket in pairs(sockets) do
		if date.secondDifference(socket.lastCall, current) >= maxSecondsBeforeClose then
			if not socket.request then
				socket.request = ""
			end
			print(l("socket timeout close: %s, request:\n'%s', request length: %d bytes", tostring(socketNum), socket.request:sub(1, 400), #socket.request))
			srv.closeSocket(socket)
		end
	end
	-- reconnect()
end

local function loadPlugins(plugins)
	local function connectFunc(...)
		local dbPrfName = {...}
		local conn
		local ret = {}
		for i, dbPrf in ipairs(dbPrfName) do
			if databases[dbPrf] then
				conn = databases[dbPrf]
			else
				conn = db.connect(dbPrf)
				if not conn then
					util.printWarning(l "worker database connection failed: " .. dbPrf)
					return nil
				end
				if dbPrf == "postgre" then
					local prf = db.preferenceFromJson(conn, "output/default_order.json")
					db.setDefaultOrderBy(conn, prf)
				end
				databases[dbPrf] = conn
			end
			ret[i] = conn
		end
		return unpack(ret)
	end

	for i, plugin in ipairs(plugins) do
		print(i .. ". " .. "Loading plugin: " .. plugin) -- ..", database: "..dbName)
		local plg = require(plugin)
		-- can be different databases in in different plugins
		local ret = plg.init(connectFunc)
		if not ret then
			local err = "ERROR: Database connection failed" -- , error: "..err
			util.printWarning(err .. "\n", "print-call-path")
			return err
		end
		calls = util.tableCombine(calls, plg.calls, "no-error")
		plg.setCallbacks(answerError, answerOk)
	end
	print("All plugins have been loaded")
	return nil
end

local function callCount()
	return tcpCallCount
end

local function pollCount()
	return tcpPollCount
end

return {
	setDebugLevel = setDebugLevel,
	loadPlugins = loadPlugins,
	parseCall = parseCall,
	pollAfter = pollAfter,
	tcpAnswer = tcpAnswer,
	tcpClose = tcpClose,
	callCount = callCount,
	pollCount = pollCount,
	answerError = answerError,
	answerOk = answerOk,
	-- variables
	debugPrintChars = debugPrintChars,
	databases = databases
}
