local http = require("socket.http")
local ltn12 = require("ltn12")
local json = require("json")
local url = require("socket.url")

module(..., package.seeall)

local luar_connection = {}

local function split(str, sep)
	return {str:match((str:gsub("[^"..sep.."]*"..sep, "([^"..sep.."]*)"..sep)))}
end

local function trim (s)
      return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end


function connect(solrurl, persistent, timeout, max_retries, debugflag)
	local parsed_url = url.parse(solrurl)
	result, connection = pcall(http.open, parsed_url.host, parsed_url.port)
	if (result == true) then
		local self = {
			connection = connection,
			host = parsed_url.host,
			port = parsed_url.port,
			scheme = parsed_url.scheme,
			persistent = persistent,
			reconnects = 0,
			max_retries = max_retries or 3,
			url = solrurl,
			path = parsed_url.path,
			response_version = 2.2,
			debug = debugflag or false
		}
		self.form_headers = {}
		self.form_headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
		self.xmlheaders = {}
		self.xmlheaders['Content-Type'] = 'text/xml; charset=utf-8'
        if not self.persistent then
			self.form_headers['Connection'] = 'close'
		end
		return setmetatable(self, {__index = luar_connection})
	end
	return nil
end

function luar_connection:close()
	self.connection:close()
end

--[[
	 q is the query string.

	fields is an optional list of fields to include. It can
	be either a string in the format that SOLR expects, or
	a lua table of field names.   Defaults to
	all fields. ("*")

	score indicates whether "score" should be included
	in the field list.  Note that if you explicitly list
	"score" in your fields value, then score is
	effectively ignored.  Defaults to true.

	highlight indicates whether highlighting should be included.
	highlight can either be False, indicating "No" (the default),
	a list of fields in the same format as "fields" or True, indicating
	to highlight any fields included in "fields". If True and no "fields"
	are given, raise a ValueError.

	sort is a list of fields to sort by. See "fields" for
	formatting. Each sort element can have be in the form
	"fieldname asc|desc" as specified by SOLR specs.

	sort_order is the backward compatible way to add the same ordering
	to all the sort field when it is not specified.

	Optional parameters can also be passed in.  Many SOLR
	parameters are in a dotted notation (e.g., hl.simple.post).
	For such parameters, replace the dots with underscores when
	calling this method. (e.g., hl_simple_post='</pre'>)

	Returns a Response instance.
--]]

function luar_connection:query(q, fields, highlight, score, sorted, sort_order, params)
	score = score or true
	sort_order = sort_order or "asc"
	
	--Clean up optional parameters to match SOLR spec.
	if (params) then
		for key, value in pairs(params) do
			--TODO , que carajo seria esto
			--params = dict([(key.replace('_','.'), value) for key, value in params.items()])
		end
	else
		params = {}
	end
	
	if (highlight) then
		params['hl'] = 'true'
		if (fields == nil) then
			error("highlight is true and no fields were given")
		elseif (type(fields) == "string") then
			params['hl.fl'] = {fields}
		else
			params['hl.fl'] = table.concat(fields, ",")
		end	
	end
	
	if (q ~= nil) then
		params['q'] = q
	end
	
	if fields then
		if (type(fields) ~= "string") then
			fields = table.concat(fields, ",")
		end
	end
	
	if (not fields) then
		fields = '*'
	end
	
	if (sorted) then
		if (sort_order == nil or (sort_order ~= "asc" and sort_order ~= "desc")) then
			error("sort_order must be 'asc' or 'desc'")
		end
		if (type(sorted) == "string") then
			local sortedFields = split(sorted, ",")
			sorted = {}
			if (sortedFields) then
				for k, v in pairs(sortedFields) do
					table.insert(sorted, trim(v))
				end
			end
		end
		sorting = {}
		for k, e in pairs(sorted) do
			if (string.sub(e, -3) ~= "asc" and string.sub(e, -4) ~= "desc") then
				table.insert(sorting, string.format("%s %s", e, sort_order))
			else
				table.insert(sorting, e)
			end
		end
		sorted = table.concat(sorting, ", ")
		params['sort'] = sorted
	end
	
	--if user wants the score but has not asked for the field, append it
	if (score and fields:find(',score') == nil) then
		fields = fields .. ',score'
	end
	
	params['fl'] = fields
	params['version'] = self.response_version
	--params['wt'] = 'standard'
	--json is returned cause itīs friendlier to lua
	params['wt'] = 'json'

	local request 
	for k, v in pairs(params) do
		request = request and request .. "&" or ""
		request = string.format("%s%s=%s", request, k, url.escape(v))
	end

	local result, data, code = self:_post(self.path .. '/select', request, self.form_headers)
	
	if (result and data) then
		data = json.decode(data)
	end	
	if (not self.persistent) then
		self:close()
	end
	return result, data
end

--[[
	Delete a specific document by id.
--]]
function luar_connection:delete(id)
	local xstr = string.format('<delete><id>%s</id></delete>', id)
	self:_update(xstr)
	return self:commit()
end

--[[
	Delete documents using a list of IDs.
--]]
function luar_connection:delete_many(ids)
	if ids then
		local lst = {'<delete>\n'}
		for k, id in pairs(ids) do
			table.insert(lst, string.format('<id>%s</id>\n', id))
		end
		table.insert(lst, '</delete>')
		self:_update(table.concat(lst, ''))
		return self:commit()
	end
end

--[[
	Delete all documents returned by a query.
--]]
function luar_connection:delete_query(query)
	local xstr = string.format('<delete><query>%s</query></delete>', url.escape(query))
	self:_update(xstr)
	return self:commit()
end

--[[
	Add a document to the SOLR server.  Document fields should be specified as arguments to this function
	
	Example:
		connection.add(id="mydoc", author="Me")
--]]
function luar_connection:add(fields, _commit)
	_commit = _commit or false
	
	local lst = {'<add>'}
	lst = self:__add(lst, fields)
	table.insert(lst, '</add>')
	local xstr = table.concat(lst, '')
	if (not _commit) then
		return self:_update(xstr)
	else
		self:_update(xstr)
		return self:commit()
	end
end


--[[
	Add several documents to the SOLR server.

	docs -- a list of documents to add
--]]
function luar_connection:add_many(docs, _commit)
	_commit = _commit or false
	local lst = {'<add>'}
	for k, doc in pairs(docs) do
		lst = self:__add(lst, doc)
	end
	table.insert(lst, '</add>')
	local xstr = table.concat(lst, '')
	if (not _commit) then
		return self:_update(xstr)
	else
		self:_update(xstr)
		return self:commit()
	end
end

--[[
	Issue a commit command to the SOLR server.
--]]
function luar_connection:commit(wait_flush, wait_searcher, _optimize)
	wait_flush = wait_flush or true
	wait_searcher = wait_searcher or true
	_optimize = _optimize or false
	
	local options, xstr
	
	if (not wait_searcher) then  --just handle deviations from the default
		if (not wait_flush) then
			options = 'waitFlush="false" waitSearcher="false"'
		else
			options = 'waitSearcher="false"'
		end
	else
		options = ''
	end
	
	if _optimize then
		xstr = string.format('<optimize %s/>', options)
	else
		xstr = string.format('<commit %s/>', options)
	end
	
	return self:_update(xstr)
end

--[[
	Issue an optimize command to the SOLR server.
--]]
function luar_connection:optimize(wait_flush, wait_searcher)
	wait_flush = wait_flush or true
	wait_searcher = wait_searcher or true
	self:commit(wait_flush, wait_searcher, true)
end

--[[
	Issue a query against a SOLR server.

	Return the raw result.  No pre-processing or
	post-processing happends to either
	input parameters or responses
--]]
function luar_connection:raw_query(params)
	--Clean up optional parameters to match SOLR spec.
	for key, value in pairs(params) do
		--TODO , que carajo seria esto
		--params = dict([(key.replace('_','.'), value) for key, value in params.items()])
	end
	
	local request = url.escape(params)

	local result, data, code = self:_post(self.path .. '/select', request, self.form_headers)
	
	if (not self.persistent) then
		self:close()
	end
	
	return data
end

--[[
	
--]]
function luar_connection:_update(request)

	local result, data, code = self:_post(self.path .. '/update', request, self.xmlheaders)
	
	if (not self.persistent) then
		self:close()
	end
	
	--TODO!!
	-- Detect old-style error response (HTTP response code
	-- of 200 with a non-zero status).
	--[[
	starts = data.startswith
	if starts('<result status="') and not starts('<result status="0"'):
		data = self.decoder(data)[0]
		parsed = parseString(data)
		status = parsed.documentElement.getAttribute('status')
		if status != 0:
			reason = parsed.documentElement.firstChild.nodeValue
			raise SolrException(rsp.status, reason)
	--]]
	return data
end

--[[
	
--]]
function luar_connection:__add(lst, fields)
	table.insert(lst, '<doc>')
	for field, value in pairs(fields) do
		-- Handle multi-valued fields if values
		-- is passed in as a table
		if type(value) ~= "table" then
			values = {value}
		else
			values = value
		end
		
		for k, v in pairs(values) do
			table.insert(lst, string.format('<field name="%s">%s</field>', field, v))
		end
	end
	table.insert(lst, '</doc>')
	return lst
end

--[[
	
--]]
function luar_connection:__repr__()
	return (string.format('<SolrConnection (url=%s, persistent=%s, post_headers=%s, reconnects=%s)>' , 
						self.url, self.persistent,
						self.xmlheaders, self.reconnects))
end

--[[
	
--]]
function luar_connection:_reconnect()
	self.reconnects = self.reconnects + 1
	self:close()
	result, connection = pcall(http.open, self.host, self.port)
	if (result == true) then
		self.connection = connection
	end
end

--[[
	effective post to the solr server
--]]
function luar_connection:_post(url, body, headers)
	--TODO this complete url is no sense, only needed cause the http request reconnecting in this step
	local complete_url = string.format("%s://%s:%s%s", self.scheme, self.host, self.port, url)
	local attempts = self.max_retries + 1
	while attempts > 0 do
		local response_body = {}
		headers["content-length"] = string.len(body)
		--TODO, this request should be sent using the self.connection already created
		local b, c, h = socket.http.request{
			url = complete_url,
			method = "POST",
			headers = headers,
			source = ltn12.source.string(body),
			sink = ltn12.sink.table(response_body)
		}
		local response = table.concat(response_body,'')
		if (c == 200) then
			return true, response
		else
			return false, response, c
		end
		--[[
		TODO, repasar codigo de cuando falla y amerite reconectar ahi invocar lo que sigue, es para cuando haya request con conexiones persistentes...
		self._reconnect()
		attempts = attempts - 1
		if attempts <= 0 then
			error("maximum post attempts reached")
		end
		]]--
	end
end
