local M = {}

local sqlite = require("sqlite")
local state = require("yankbank.state")

local function get_db_path()
    local opts = state.get_opts()
    return opts.db_path
        or debug.getinfo(1).source:sub(2):match("(.*/).*/.*/.*/")
        or "./"
end

local max_entries = 10

---@class YankBankDB:sqlite_db
---@field bank sqlite_tbl
---@field bank sqlite_tbl
local db = sqlite({
    uri = get_db_path() .. "/yankbank.db",
    bank = {
        -- yanked text should be unique and be primary key
        yank_text = { "text", unique = true, primary = true, required = true },
        reg_type = { "text", required = true },
        pinned = { "integer", required = true, default = 0 },
    },
})

---@class sqlite_tbl
local data = db.bank

---@param content string
---@return string
function M.escape(content)
    return string.format("__ESCAPED__'%s'", content)
end

---@param content string
---@return string
---@return integer?
function M.unescape(content)
    return content:gsub("^__ESCAPED__'(.*)'$", "%1")
end

--- insert yank entry into database
---@param yank_text string yanked text
---@param reg_type string register type
---@param pin integer|boolean? pin status of inserted entry
function data:insert_yank(yank_text, reg_type, pin)
    -- attempt to remove entry if count > 0 (to move potential duplicate)
    local is_pinned = 0
    if self:count() > 0 then
        db:with_open(function()
            -- check if entry exists in db
            local res = db:eval(
                "SELECT * FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type",
                { yank_text = M.escape(yank_text), reg_type = reg_type }
            )

            -- if result is empty (eval returns boolean), proceed to insertion
            if type(res) == "boolean" then
                return
            end

            -- entry found, get pin status
            is_pinned = res[1].pinned

            -- remove entry from db so it can be moved to first position
            db:eval(
                "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type",
                { yank_text = M.escape(yank_text), reg_type = reg_type }
            )
        end)
    end

    -- override is_pinned if pin param is set, default to is_pinned otherwise
    is_pinned = (pin == 1 or pin == true) and 1
        or (pin == 0 or pin == false) and 0
        or is_pinned

    -- insert entry using the eval method with parameterized query to avoid error on 'data:insert()'
    db:with_open(function()
        db:eval(
            "INSERT INTO bank (yank_text, reg_type, pinned) VALUES (:yank_text, :reg_type, :pinned)",
            {
                yank_text = M.escape(yank_text),
                reg_type = reg_type,
                pinned = is_pinned,
            }
        )
    end)

    -- attempt to trim database size
    self:trim_size()
end

--- trim database size if it exceeds max_entries option
--- WARN: if all entries are pinned, behavior is undefined
function data:trim_size()
    if self:count() > max_entries then
        -- remove the oldest entry
        local e = db:with_open(function()
            return db:select("bank", {
                where = { pinned = 0 },
                order_by = { asc = "rowid" },
                limit = 1,
            })[1]
        end)

        if e then
            db:with_open(function()
                db:eval(
                    "DELETE FROM bank WHERE yank_text = :yank_text",
                    { yank_text = e.yank_text }
                )
            end)
        end
    end
end

--- get sqlite bank contents
---@return table yanks, table reg_types, table pins
function data:get_bank()
    local yanks, reg_types, pins = {}, {}, {}

    local bank = self:get()
    for _, entry in ipairs(bank) do
        local text, _ = M.unescape(entry.yank_text)
        table.insert(yanks, 1, text)
        table.insert(reg_types, 1, entry.reg_type)
        table.insert(pins, 1, entry.pinned)
    end

    return yanks, reg_types, pins
end

--- remove an entry from the banks table matching input text
---@param text string
---@param reg_type string
function data.remove_match(text, reg_type)
    db:with_open(function()
        return db:eval(
            "DELETE FROM bank WHERE yank_text = :yank_text and reg_type = :reg_type",
            { yank_text = M.escape(text), reg_type = reg_type }
        )
    end)
end

--- pin entry in yankbank to prevent removal
---@param text string text to match and pin
---@param reg_type string reg_type corresponding to text
---@return boolean?
function data.pin(text, reg_type)
    return db:with_open(function()
        return (
            db:eval(
                "UPDATE bank SET pinned = 1 WHERE yank_text = :yank_text and reg_type = :reg_type",
                { yank_text = M.escape(text), reg_type = reg_type }
            )
        )
    end)
end

--- unpin entry in yankbank to prevent removal
---@param text string
---@param reg_type string reg_type corresponding to text
---@return boolean?
function data.unpin(text, reg_type)
    return db:with_open(function()
        return db:eval(
            "UPDATE bank SET pinned = 0 WHERE yank_text = :yank_text and reg_type = :reg_type",
            { yank_text = M.escape(text), reg_type = reg_type }
        )
    end)
end

--- get data in sqlite_tbl form (for api use only)
---@return sqlite_tbl
function M.data()
    return data
end

--- set up database persistence
---@return sqlite_tbl data
function M.setup()
    local opts = state.get_opts()
    max_entries = opts.max_entries

    vim.api.nvim_create_user_command("YankBankClearDB", function()
        data:drop()
        state.set_yanks({})
        state.set_reg_types({})
    end, {})

    if opts.debug == true then
        vim.api.nvim_create_user_command("YankBankViewDB", function()
            print(vim.inspect(data:get()))
        end, {})
    end

    return data
end

return M
