--[[
Copyright (c) 2012-2013 Kaarle Ritvanen
See LICENSE file for license details
--]]

local acf = require('acf2')
local mnode = acf.model.node
local isinstance = acf.object.isinstance

local json = require('cjson')
local stringy = require('stringy')


math.randomseed(os.time())

local save_req = os.execute('[ $(stat -f -c "%T" /) = tmpfs ]')

-- TODO shared storage for sessions
-- TODO expire stale sessions
local sessions = {}


return function(env)
	  local method = env.REQUEST_METHOD
	  local path = env.PATH_INFO

	  local function wrap(code, headers, res, encode)
	     if not headers then headers = {} end

	     if res then
		local ctype
		if encode then
		   ctype = 'application/json'
		   res = json.encode(res)
		else ctype = 'text/plain' end
		headers['Content-Type'] = ctype
	     else res = '' end

	     return code, headers, coroutine.wrap(
		function() coroutine.yield(res) end
	     )
	  end

	  if path == '/' then
	     if method ~= 'GET' then return wrap(405) end
	     return wrap(301, {['Location']='/browser/'})
	  end

	  local data
	  local length = env.CONTENT_LENGTH and tonumber(
	     env.CONTENT_LENGTH
	  ) or 0
	  if length > 0 then
	     local success
	     success, data = pcall(
		json.decode,
		env.input:read(length)
	     )
	     if not success then
		return wrap(400, nil, 'Request not in JSON format')
	     end
	  end

	  local sid = tonumber(env.HTTP_X_ACF_AUTH_TOKEN)
	  local session, user, txn_id
	  if sid then
	     session = sessions[sid]
	     if not session then return wrap(401) end
	     user = session.user
	     txn_id = tonumber(env.HTTP_X_ACF_TRANSACTION_ID)
	  end

	  local parent_txn
	  if txn_id then
	     parent_txn = session.txns[txn_id]
	     if not parent_txn then
		return wrap(400, nil, 'Invalid transaction ID')
	     end
	  end
	  local txn = acf.start_txn(parent_txn, true)

	  local function fetch_user(name)
	     user = name and txn:fetch('/auth/users')[name]
	  end
	  if user then
	     fetch_user(user)
	     if not user then return wrap(401) end
	  end

	  if path == '/login' then
	     if method == 'POST' then
		if not data.username or not data.password then
		   return wrap(401)
		end
		fetch_user(data.username)
		if user and user:check_password(data.password) then
		   local sid
		   repeat
		      sid = math.floor(math.random() * 2^32)
		   until not sessions[sid]

		   sessions[sid] = {user=data.username, last_txn_id=0, txns={}}
		   return wrap(
		      204,
		      {
			 ['X-ACF-Auth-Token']=sid,
			 ['X-ACF-Save-Required']=save_req and 1 or 0
		      }
		   )
		end
		return wrap(401)
	     end

	     if not user then return wrap(401) end

	     if method == 'DELETE' then
		sessions[sid] = nil
		return wrap(204)
	     end

	     return wrap(405)
	  end

	  if not user then return wrap(401) end

	  if path == '/save' then
	     if not save_req then return wrap(404) end
	     if method ~= 'POST' then return wrap(405) end
	     if os.execute('lbu commit') then wrap(204) end
	     return wrap(500, nil, 'lbu commit failed')
	  end

	  local success, code, hdr, res = acf.call(
	     function()
		if stringy.startswith(path, '/meta/') then
		   if method ~= 'GET' then return 405 end
		   return 200, nil, txn:meta(path:sub(6, -1))
		end

		if stringy.startswith(path, '/config/') then
		   path = path:sub(8, -1)
		   
		   local parent, name, res
		   if path ~= '/' then
		      parent = txn:fetch(acf.path.parent(path))
		      name = acf.path.name(path)
		   end
		   
		   if method == 'GET' then
		      local obj = txn:fetch(path)

		      if type(obj) == 'function' then return 404 end

		      if isinstance(obj, mnode.TreeNode) then
			 if not mnode.has_permission(obj, user, 'read') then
			    return 403
			 end

			 local node = {}
			 for k, v in mnode.pairs(obj) do
			    local readable = true
			    
			    if isinstance(v, mnode.TreeNode) then
			       readable = mnode.has_permission(
				  v,
				  user,
				  'read'
			       )
			       v = mnode.path(v)
			    end
			    
			    if readable then node[k] = v end
			 end
			 res = {data=node, meta=mnode.meta(obj)}
		   
		      elseif mnode.has_permission(parent, user, 'read') then
			 res = {data=obj, meta=mnode.mmeta(parent, name)}
		      else return 403 end

		      return 200, nil, res
		   end

		   if method == 'POST' then
		      local obj = txn:fetch(path)

		      if isinstance(obj, mnode.List) then
			 if not mnode.has_permission(obj, user, 'create') then
			    return 403
			 end

			 local index
			 if not isinstance(obj, mnode.Set) then
			    index = data.index
			    data = data.data
			 end
			 mnode.insert(obj, data, index)

		      elseif type(obj) == 'function' then
			 if not mnode.has_permission(parent, user, name) then
			    return 403
			 end
			 res = obj(data)

		      else return 405 end

		   else
		      local obj = parent[name]
		      if obj ~= nil and not isinstance(obj, mnode.TreeNode) then
			 obj = parent
		      end

		      if method == 'DELETE' then
			 if obj == nil then return 404 end
			 if not mnode.has_permission(obj, user, 'delete') then
			    return 403
			 end
			 parent[name] = nil

		      elseif method == 'PUT' then
			 if isinstance(parent, mnode.Set) then return 405 end
			 local permission = 'modify'
			 if obj == nil then
			    obj = parent
			    permission = 'create'
			 end
			 if not mnode.has_permission(obj, user, permission) then
			    return 403
			 end
			 parent[name] = data
			 
		      else return 405 end
		   end
		   
		   txn:commit()
		   return res == nil and 205 or 200, nil, res
		end

		if path == '/transaction' then
		   if ({DELETE=true, PUT=true})[method] then
		      if not txn_id then return 405 end
		      if method == 'PUT' then parent_txn:commit() end
		      session.txns[txn_id] = nil
		      return 204
		   end

		   if method ~= 'POST' then return 405 end

		   session.last_txn_id = session.last_txn_id + 1
		   local txn_id = session.last_txn_id
		   session.txns[txn_id] = acf.start_txn(parent_txn)
		   return 204, {['X-ACF-Transaction-ID']=txn_id}
		end

		return 404
	     end
	  )

	  if success then return wrap(code, hdr, res, true) end

	  if code.conflict then return wrap(409, nil, code.conflict, true) end
	  
	  return wrap(422, nil, code, true)
       end
