local LrTasks = import "LrTasks"

local M = {}

local function runWriteAccess(catalog, actionName, fn, timeoutSeconds)
    local okTable, errTable = LrTasks.pcall(function()
        catalog:withWriteAccessDo(actionName, fn, { timeout = timeoutSeconds })
    end)
    if okTable then
        return true, nil
    end

    local tableErr = tostring(errTable or "")
    if string.find(tableErr, "bad argument", 1, true)
        or string.find(tableErr, "wrong number of arguments", 1, true)
        or string.find(tableErr, "attempt to index", 1, true)
    then
        local okNumeric, errNumeric = LrTasks.pcall(function()
            catalog:withWriteAccessDo(actionName, fn, timeoutSeconds)
        end)
        if okNumeric then
            return true, nil
        end

        local numericErr = tostring(errNumeric or "")
        if string.find(numericErr, "bad argument", 1, true)
            or string.find(numericErr, "wrong number of arguments", 1, true)
        then
            local okLegacy, errLegacy = LrTasks.pcall(function()
                catalog:withWriteAccessDo(actionName, fn)
            end)
            if okLegacy then
                return true, nil
            end
            return false, tostring(errLegacy or "write_access_error")
        end
        return false, numericErr
    end

    return false, tableErr
end

local function isContentionError(raw)
    local text = string.lower(tostring(raw or ""))
    if string.find(text, "blocked by another write access", 1, true) then
        return true
    end
    if string.find(text, "could not execute action", 1, true) then
        return true
    end
    if string.find(text, "write access", 1, true) and string.find(text, "timeout", 1, true) then
        return true
    end
    return false
end

function M.run(catalog, actionName, fn, opts)
    if not catalog or type(fn) ~= "function" then
        return false, "invalid_arguments"
    end

    local options = opts or {}
    local attempts = tonumber(options.attempts) or 8
    local sleepBase = tonumber(options.sleep) or 0.08
    local sleepMax = tonumber(options.sleepMax) or 0.45
    local timeoutSeconds = tonumber(options.timeout) or 12
    local lastError = nil
    local canYield = false
    local canYieldOk, canYieldValue = pcall(function()
        return LrTasks.canYield()
    end)
    if canYieldOk and canYieldValue == true then
        canYield = true
    end

    for attempt = 1, attempts do
        local writeOk, writeErr = runWriteAccess(catalog, actionName, fn, timeoutSeconds)
        if writeOk then
            return true, nil, attempt
        end

        lastError = tostring(writeErr or "write_access_error")
        if not isContentionError(lastError) then
            return false, lastError, attempt
        end

        if attempt < attempts and canYield then
            local delay = sleepBase * attempt
            if delay > sleepMax then
                delay = sleepMax
            end
            LrTasks.sleep(delay)
        else
            return false, tostring(lastError or "write_access_failed"), attempt
        end
    end

    return false, tostring(lastError or "write_access_failed"), attempts
end

return M
