-- Autogenerated with DRAKON Editor 1.33
require('strict').on()

local table = table
local string = string
local pairs = pairs
local ipairs = ipairs
local io = io
local pcall = pcall
local xpcall = xpcall
local debug = debug
local math = math
local tostring = tostring
local tonumber = tonumber
local clock = require("clock")
local log = require("log")
local digest = require("digest")
local fiber = require("fiber")
local json = require("json")
local fio = require("fio")
local log = require("log")
local os = os
local error = error
local print = print

local utf8 = require("lua-utf8")

local utils = require("utils")
local ej = require("ej")
local space = require("space")
local box = box

-- configuration
local global_cfg = global_cfg

setfenv(1, {}) 

local module = {}

module.handlers = {}



function add_to_recent(doc_id, user_id)
    local fields, i, key, max_recent, now, recent, recent_row, row, to_kill
    now = utils.utc_time()
    fields = {
        timestamp = now
    }
    row = {doc_id, user_id, fields}
    box.space.rrecent:replace(row)
    max_recent = 30
    recent = db_recent_by_user(user_id)
    to_kill = #recent - max_recent
    if to_kill > 0 then
        utils.sort_by_prop(recent, "timestamp", "asc")
        i = 1
        while true do
            if i <= to_kill then
                
            else
                break
            end
            recent_row = recent[i]
            key = {recent_row.doc_id, recent_row.user_id}
            box.space.rrecent:delete(key)
            i = i + 1
        end
    end
end

function api(data, user_id, roles)
    local handler, ok, op, output_data
    if data then
        op = data.op
        if op then
            handler = module.handlers[op]
            if handler then
                ok, output_data = handler(
                    data.body,
                    user_id,
                    roles
                )
            else
                ok = false
                output_data = "ERR_NOT_FOUND"
            end
        else
            ok = false
            output_data = "ERR_BAD_REQUEST"
        end
    else
        ok = false
        output_data = "ERR_BAD_REQUEST"
    end
    if ok then
        return ok, {data=output_data}
    else
        return false, output_data
    end
end

function can_write(doc_id, user_id, roles)
    local can, space_id
    space_id = db_get_doc_space(
        doc_id
    )
    can = can_write_to_space(
        space_id,
        user_id,
        roles
    )
    return can, space_id
end

function can_write_to_space(space_id, user_id, roles)
    local access
    access = space.get_space_access(
        space_id,
        user_id,
        roles
    )
    if (access == "write") or (access == "admin") then
        return true, space_id
    else
        return false
    end
end

function copy(data, user_id, roles)
    local ok, result, tags, target_space
    ok, target_space = can_write(
        data.target,
        user_id,
        roles
    )
    result = nil
    if ok then
        local normal_827
        normal_827 = 1
        for _, doc_id in ipairs(data.docs) do
            if find_cycle(doc_id, data.target) then
                ok = false
                result = "ERR_CYCLE"
                normal_827 = 0
                break
            end
        end
        if normal_827 == 1 then
            tags = {}
            tags[data.target] = tag_doc(
                data.target,
                user_id
            )
            for _, doc_id in ipairs(data.docs) do
                db_copy_doc(
                    doc_id,
                    data.target,
                    tags,
                    user_id
                )
            end
            result = {tags=tags}
        end
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    return ok, result
end

function create_root_folder(space_id, user_id)
    local fields, id
    fields = {
        type = "folder"
    }
    set_when_created(fields, user_id)
    id = generate_doc_id()
    box.space.rtags:insert {
    	id,
    	space_id,
    	1,
    	true
    }
    box.space.rdocs:insert {
    	id,
    	"",
    	fields
    }
    return id
end

function db_copy_doc(old_doc_id, parent_id, tags, user_id)
    local child_id, children, doc, doc_id, fields, item_body, item_id, items, space_id, tag
    box.begin()
    doc = db_get_doc(old_doc_id)
    space_id = db_get_doc_space(parent_id)
    fields = doc[3]
    set_when_updated(fields, user_id)
    doc_id, tag = db_insert_doc(
        space_id,
        parent_id,
        fields
    )
    tags[doc_id] = tag
    items = db_get_items(old_doc_id)
    for _, item in ipairs(items) do
        item_id = item[2]
        item_body = item[3]
        db_insert_item(
            doc_id,
            item_id,
            item_body
        )
    end
    box.commit()
    children = db_get_doc_children(old_doc_id)
    for _, child in ipairs(children) do
        child_id = child[1]
        db_copy_doc(
            child_id,
            doc_id,
            tags,
            user_id
        )
    end
end

function db_delete_doc(doc_id)
    local child_id, children, recent
    children = db_get_doc_children(
        doc_id
    )
    for _, child in ipairs(children) do
        child_id = child[1]
        db_delete_doc(child_id)
    end
    db_delete_items(doc_id)
    recent = db_recent_rows_by_doc(doc_id)
    db_delete_recent(recent)
    tag_update(doc_id, false)
    return box.space.rdocs:delete(doc_id)
end

function db_delete_item(doc_id, item_id)
    local key
    key = {doc_id, item_id}
    box.space.ritems:delete(key)
end

function db_delete_items(doc_id)
    local item_rows, key
    item_rows = db_get_items(doc_id)
    for _, row in ipairs(item_rows) do
        key = make_key2(row)
        box.space.ritems:delete(key)
    end
end

function db_delete_recent(rows)
    local key
    for _, row in ipairs(rows) do
        key = {row[1], row[2]}
        box.space.rrecent:delete(key)
    end
end

function db_delete_space(space_id)
    local id, rtags
    rtags = box.space.rtags.index.by_tag:select(space_id)
    for _, rtag in ipairs(rtags) do
        id = rtag[1]
        box.space.rtags:delete(id)
        box.space.rdocs:delete(id)
    end
end

function db_get_doc(doc_id)
    return box.space.rdocs:get(doc_id)
end

function db_get_doc_children(doc_id)
    return box.space.rdocs.index.by_parent:select(doc_id)
end

function db_get_doc_children_rec(doc_id, output)
    local child_id, docs
    docs = db_get_doc_children(doc_id)
    utils.add_range(output, docs)
    for _, doc in ipairs(docs) do
        child_id = doc[1]
        db_get_doc_children_rec(
            child_id,
            output
        )
    end
end

function db_get_doc_space(doc_id)
    local row
    row = box.space.rtags:get(doc_id)
    return row[2]
end

function db_get_docs(space_id)
    local result, root, row, sdata
    row = box.space.spaces:get(space_id)
    if row then
        sdata = row[2]
        if utils.is_empty(sdata.root_id) then
            return {}
        else
            root = db_get_doc(sdata.root_id)
            result = {root}
            db_get_doc_children_rec(
                sdata.root_id,
                result
            )
            return result
        end
    else
        return {}
    end
end

function db_get_items(doc_id)
    return box.space.ritems:select(doc_id)
end

function db_get_tag_row(doc_id)
    return box.space.rtags:get(doc_id)
end

function db_insert_doc(space_id, parent_id, fields)
    local doc_id, latest_tag, tag
    doc_id = generate_doc_id()
    latest_tag = get_latest_tag(space_id)
    if latest_tag then
        tag = latest_tag + 1
    else
        tag = 1
    end
    box.space.rtags:insert {
    	doc_id,
    	space_id,
    	tag,
    	true
    }
    box.space.rdocs:insert {
    	doc_id,
    	parent_id,
    	fields
    }
    return doc_id, tag
end

function db_insert_item(doc_id, item_id, fields)
    local row
    row = {
    	doc_id,
    	item_id,
    	fields
    }
    box.space.ritems:insert(row)
end

function db_move_doc(doc_id, parent_id, tags, user_id)
    local doc, fields, new_row, old_parent
    box.begin()
    doc = db_get_doc(doc_id)
    old_parent = doc[2]
    fields = doc[3]
    if old_parent == parent_id then
        
    else
        set_when_updated(fields, user_id)
        tags[doc_id] = tag_update(doc_id, true)
        tags[old_parent] = tag_doc(old_parent, user_id)
        new_row = {
        	doc_id,
        	parent_id,
        	fields
        }
        box.space.rdocs:replace(new_row)
    end
    box.commit()
end

function db_recent_by_user(user_id)
    local rows
    rows = db_recent_rows_by_user(
        user_id
    )
    return utils.map(row_to_recent)
end

function db_recent_rows_by_doc(doc_id)
    return box.space.rrecent:select(
        doc_id
    )
end

function db_recent_rows_by_user(user_id)
    return box.space.rrecent.index.by_user:select(
        user_id
    )
end

function db_update_doc(doc_id, fields, user_id)
    local changed_fields, data, new_row, ok, old_row, parent_id, tags
    ok = true
    data = nil
    tags = {}
    old_row = db_get_doc(doc_id)
    if old_row then
        changed_fields = old_row[3]
        utils.copy_props(
            fields,
            changed_fields
        )
        if fields.parent_id then
            parent_id = fields.parent_id
        else
            parent_id = old_row[2]
        end
        changed_fields.parent_id = nil
        changed_fields.doc_id = nil
        set_when_updated(changed_fields, user_id)
        tags[doc_id] = tag_update(doc_id, true)
        tags[parent_id] = tag_doc(parent_id, user_id)
        new_row = {
        	doc_id,
        	parent_id,
        	changed_fields
        }
        box.space.rdocs:replace(new_row)
        ok = true
        data = {tags=tags}
    else
        ok = false
        data = "ERR_NOT_FOUND"
    end
    return ok, data
end

function db_update_item(doc_id, item_id, fields)
    local db_fields, key, old_row, row
    key = {doc_id, item_id}
    old_row = box.space.ritems:get(key)
    db_fields = old_row[3]
    utils.copy_props(
        fields,
        db_fields
    )
    row = {
    	doc_id,
    	item_id,
    	db_fields
    }
    box.space.ritems:replace(row)
end

function delete_doc(data, user_id, roles)
    local doc_row, ok, parent_id, result, tag, tags
    box.begin()
    if can_write(data.doc_id, user_id, roles) then
        doc_row = db_get_doc(data.doc_id)
        db_delete_doc(
            data.doc_id
        )
        parent_id = doc_row[2]
        tag = tag_doc(parent_id, user_id)
        tags = {}
        tags[parent_id] = tag
        ok = true
        result = {tags=tags}
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    box.commit()
    return ok, result
end

function delete_recent_by_user(user_id)
    local rows
    rows = db_recent_rows_by_user(
        user_id
    )
    db_delete_recent(rows)
end

function download_table_rows(doc_id, user_id, roles)
    local by_item_id, items, lines, ok, result
    ok, result = get_doc_kernel(
        doc_id,
        user_id,
        roles
    )
    if ok then
        items = db_get_items(
            doc_id
        )
        by_item_id = make_num_comparer(2)
        table.sort(items, by_item_id)
        lines = utils.map(
            items,
            export_table_row
        )
        result = utils.join(lines, "\n")
    end
    return ok, result
end

function edit(data, user_id, roles)
    local db_fields, field_names, new_row, ok, old_row, parent_id, ptag, result, tag, tag_row, tags
    local _sw4910000_ = 0
    box.begin()
    old_row = db_get_doc(data.doc_id)
    if old_row then
        if can_write(data.doc_id, user_id, roles) then
            parent_id = old_row[2]
            db_fields = old_row[3]
            tag_row = db_get_tag_row(data.doc_id)
            tag = tag_row[3]
            if tag == data.tag then
                field_names = utils.set_to_list(
                    data.fields
                )
                tags = {}
                if #field_names == 0 then
                    
                else
                    utils.copy_props(
                        data.fields,
                        db_fields
                    )
                    ptag = tag_doc(parent_id, user_id)
                    tags[parent_id] = ptag
                end
                set_when_updated(db_fields, user_id)
                tags[data.doc_id] = tag_update(
                    data.doc_id,
                    true
                )
                new_row = {
                	data.doc_id,
                	parent_id,
                	db_fields
                }
                box.space.rdocs:replace(new_row)
                ok = true
                result = {tags=tags}
                for _, item_edit in ipairs(data.items) do
                    _sw4910000_ = item_edit.op
                    if _sw4910000_ == "insert" then
                        db_insert_item(
                            data.doc_id,
                            item_edit.id,
                            item_edit.fields
                        )
                    else
                        if _sw4910000_ == "update" then
                            db_update_item(
                                data.doc_id,
                                item_edit.id,
                                item_edit.fields
                            )
                        else
                            if _sw4910000_ == "delete" then
                                
                            else
                                error(_sw4910000_)
                            end
                            db_delete_item(
                                data.doc_id,
                                item_edit.id
                            )
                        end
                    end
                end
            else
                ok = false
                result = "ERR_MODIFIED"
            end
        else
            ok = false
            result = "ERR_ACCESS_DENIED"
        end
    else
        ok = false
        result = "ERR_NOT_FOUND"
    end
    box.commit()
    return ok, result
end

function export_table_row(item_row)
    local body, row
    body = item_row[3]
    row = body.row or {}
    return utils.join(row, "|")
end

function find_cycle(from_id, to_id)
    local current, current_id, parent_id
    current_id = to_id
    while true do
        current = db_get_doc(current_id)
        parent_id = current[2]
        if parent_id == "" then
            return false
        end
        if parent_id == from_id then
            return true
        end
        current_id = parent_id
    end
end

function generate_doc_id()
    local rtags
    rtags = box.space.rtags
    local id
    while true do
        id = utils.random_password(6)
        local row = rtags:get(id)
        if row then
            
        else
            break
        end
    end
    return id
end

function get_all_live_tags(data, user_id, roles)
    local access, item, live, next_cookie, ok, result, rows, rtags, tags
    access = space.get_space_access(
        data.space_id,
        user_id,
        roles
    )
    if access then
        tags = {}
        rtags = box.space.rtags
        rows = rtags.index.by_tag:select(
            data.space_id
        )
        for _, row in ipairs(rows) do
            live = row[4]
            if live then
                item = {
                    doc_id = row[1],
                    tag = row[3]
                }
                table.insert(
                    tags,
                    item
                )
            end
        end
        next_cookie = get_latest_tag(data.space_id)
        ok = true
        result = {
        	tags=tags,
        	next_cookie = next_cookie
        }
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    return ok, result
end

function get_changes(data, user_id, roles)
    local access, doc_id, live, next_cookie, ok, result, rtags, tag, tags
    access = space.get_space_access(
        data.space_id,
        user_id,
        roles
    )
    if access then
        tags = {}
        rtags = box.space.rtags
        for it, row in rtags.index.by_tag:pairs(
          {data.space_id, data.cookie},
          {iterator="GT"}) do
            if row[2] ~= data.space_id then
                break
            end
        
            local item = {
                doc_id = row[1],
                tag = row[3],
                live = row[4]
            }
        
            table.insert(
                tags,
                item
            )
        end
        next_cookie = get_latest_tag(data.space_id)
        ok = true
        result = {
        	tags=tags,
        	next_cookie = next_cookie
        }
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    return ok, result
end

function get_doc(data, user_id, roles)
    local ok, result
    ok, result = get_doc_kernel(
        data.doc_id,
        user_id,
        roles
    )
    if ok then
        result.items = utils.map(
            db_get_items(data.doc_id),
            row_to_item
        )
        if data.visit then
            add_to_recent(
                data.doc_id,
                user_id
            )
        end
    end
    return ok, result
end

function get_doc_kernel(doc_id, user_id, roles)
    local access, doc, doc_row, ok, result, tag_row
    doc_row = db_get_doc(doc_id)
    if doc_row then
        tag_row = db_get_tag_row(doc_id)
        if tag_row then
            doc = row_to_doc(doc_row)
            doc.space_id = tag_row[2]
            doc.tag = tag_row[3]
            access = space.get_space_access(
                doc.space_id,
                user_id,
                roles
            )
            if access then
                doc.access = access
                ok = true
                result = doc
            else
                ok = false
                result = "ERR_ACCESS_DENIED"
            end
        else
            ok = false
            result = "ERR_NOT_FOUND"
        end
    else
        ok = false
        result = "ERR_NOT_FOUND"
    end
    return ok, result
end

function get_latest_tag(space_id)
    local index, row
    index = box.space.rtags.index.by_tag
    row = index:max(space_id)
    if (row) and (row[2] == space_id) then
        return row[3]
    else
        return nil
    end
end

function get_records(data, user_id, roles)
    local access, docs, fields, ok, result
    access = space.get_space_access(
        data.space_id,
        user_id,
        roles
    )
    if access then
        docs = db_get_docs(data.space_id)
        result = {records={}}
        for _, doc in ipairs(docs) do
            fields = doc[3]
            if fields.type == "record" then
                table.insert(
                    result.records,
                    {name=fields.name}
                )
            end
        end
        ok = true
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    return ok, result
end

function get_tags(data, user_id, roles)
    local tag, tag_row
    local result = {}
    for _, doc_id in ipairs(data.ids) do
        tag_row = db_get_tag_row(doc_id)
        if (tag_row) and (tag_row[4]) then
            tag = tag_row[3]
        else
            tag = "deleted"
        end
        result[doc_id] = tag
    end
    return true, result
end

function init()
    module.handlers.get_doc = get_doc
    module.handlers.get_records = get_records
    module.handlers.insert_doc = insert_doc
    module.handlers.update_doc = update_doc
    module.handlers.delete_doc = delete_doc
    module.handlers.get_tags = get_tags
    module.handlers.edit = edit
    module.handlers.get_all_live_tags = get_all_live_tags
    module.handlers.get_changes = get_changes
    module.handlers.copy = copy
    module.handlers.move = move
end

function insert_doc(data, user_id, roles)
    local doc_id, ok, parent_id, ptag, result, space_id, tag, tags, write_ok
    box.begin()
    parent_id = data.fields.parent_id
    write_ok, space_id = can_write(
        parent_id,
        user_id,
        roles
    )
    if write_ok then
        set_when_created(data.fields, user_id)
        data.fields.parent_id = nil
        doc_id, tag = db_insert_doc(
            space_id,
            parent_id,
            data.fields
        )
        tags = {}
        tags[doc_id] = tag
        ptag = tag_doc(parent_id, user_id)
        tags[parent_id] = ptag
        ok = true
        result = {
        	doc_id=doc_id,
        	tags=tags
        }
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    box.commit()
    return ok, result
end

function make_key2(row)
    local key
    key = {row[1], row[2]}
    return key
end

function make_num_comparer(prop)
    return function(left, right)
    	return tonumber(left[prop]) < tonumber(right[prop])
    end
end

function make_tag(fields, user_id, now)
    local tag
    tag = user_id .. tostring(now)
    fields.tag = tag
    return tag
end

function move(data, user_id, roles)
    local ok, result, tags, target_space
    ok, target_space = can_write(
        data.target,
        user_id,
        roles
    )
    result = nil
    if ok then
        local normal_872
        normal_872 = 1
        for _, doc_id in ipairs(data.docs) do
            ok = can_write(
                doc_id,
                user_id,
                roles
            )
            if ok then
                
            else
                ok = false
                result = "ERR_ACCESS_DENIED"
                normal_872 = 0
                break
            end
        end
        if normal_872 == 1 then
            local normal_861
            normal_861 = 1
            for _, doc_id in ipairs(data.docs) do
                if find_cycle(doc_id, data.target) then
                    ok = false
                    result = "ERR_CYCLE"
                    normal_861 = 0
                    break
                end
            end
            if normal_861 == 1 then
                tags = {}
                tags[data.target] = tag_doc(
                    data.target,
                    user_id
                )
                for _, doc_id in ipairs(data.docs) do
                    db_move_doc(
                        doc_id,
                        data.target,
                        tags,
                        user_id
                    )
                end
                result = {tags=tags}
            end
        end
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    return ok, result
end

function parse_multipart(body)
    local body_lines, lines, separator, state, trimmed
    body_lines = {}
    state = "start"
    lines = utils.split(body, "\n")
    for _, line in ipairs(lines) do
        trimmed = utils.trim(line)
        if state == "header" then
            if #trimmed == 0 then
                state = "body"
            end
        else
            if state == "start" then
                log.info(trimmed)
                separator = trimmed .. "--"
                state = "header"
            else
                if state == "body" then
                    
                else
                    error(state)
                end
                if (#trimmed == 0) or (trimmed == separator) then
                    break
                end
                table.insert(
                    body_lines,
                    trimmed
                )
            end
        end
    end
    return body_lines
end

function remove_from_recent(doc_id, user_id)
    local key
    key = {doc_id, user_id}
    box.space.rrecent:delete(key)
end

function row_to_doc(row)
    local doc
    doc = row[3]
    doc.doc_id = row[1]
    doc.parent_id = row[2]
    return doc
end

function row_to_item(row)
    local item
    item = row[3]
    item.doc_id = row[1]
    item.item_id = row[2]
    return item
end

function row_to_recent(row)
    local fields
    fields = row[3]
    fields.user_id = row[2]
    fields.doc_id = row[1]
    return fields
end

function set_when_created(fields, user_id)
    local now
    now = utils.utc_time()
    fields.when_created = now
    fields.created_by = user_id
    fields.when_updated = now
    fields.updated_by = user_id
end

function set_when_updated(fields, user_id)
    local now
    now = utils.utc_time()
    fields.when_updated = now
    fields.updated_by = user_id
end

function tag_doc(doc_id, user_id)
    local changed_fields, new_row, old_row, tag
    old_row = db_get_doc(doc_id)
    if old_row then
        changed_fields = old_row[3]
        set_when_updated(changed_fields, user_id)
        new_row = {
        	old_row[1],
        	old_row[2],
        	changed_fields
        }
        box.space.rdocs:replace(new_row)
        tag = tag_update(doc_id, true)
        return tag
    else
        return nil
    end
end

function tag_update(doc_id, exists)
    local rtags, space_id, tag, tag_row
    rtags = box.space.rtags
    tag_row = rtags:get(doc_id)
    if tag_row then
        space_id = tag_row[2]
        tag = get_latest_tag(space_id) + 1
        rtags:delete(doc_id)
        rtags:insert {
        	doc_id,
        	space_id,
        	tag,
        	exists
        }
        return tag
    else
        return nil
    end
end

function update_doc(data, user_id, roles)
    local ok, result
    box.begin()
    if can_write(data.doc_id, user_id, roles) then
        ok, result = db_update_doc(
            data.doc_id,
            data.fields,
            user_id
        )
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    box.commit()
    return ok, result
end

function upload_table_rows(doc_id, body, user_id, roles)
    local i, item_id, lines, ok, parts, result
    if can_write(doc_id, user_id, roles) then
        lines = parse_multipart(body)
        box.begin()
        db_delete_items(doc_id)
        i = 1
        for _, line in ipairs(lines) do
            parts = utils.split(line, "|")
            body = {row=parts}
            item_id = tostring(i)
            db_insert_item(
                doc_id,
                item_id,
                body
            )
            i = i + 1
        end
        box.commit()
        ok = true
        result = {}
    else
        ok = false
        result = "ERR_ACCESS_DENIED"
    end
    return ok, result
end


module.api = api
module.create_root_folder = create_root_folder
module.delete_recent_by_user = delete_recent_by_user
module.db_delete_doc = db_delete_doc
module.get_changes = get_changes
module.upload_table_rows = upload_table_rows
module.download_table_rows = download_table_rows
module.db_delete_space = db_delete_space

init()

return module
