--- @module "zync.lib.utils"
---
--- Utility and helper functions submodule
---
local Utils = {}

--- Dependencies
local lfs = require('lfs')

--- Finds file in the current directory
---
--- @param name string  File name
---
--- @return string File path if found, empty string otherwise
---
function Utils.find_file(name)
    local path = lfs.currentdir()
    for file in lfs.dir(path) do
        if file ~= '.' and file ~= '..' and file == name then
            return path .. '/' .. file
        end
    end

    return ''
end

--- Reads file contents
---
--- @param path string  File path
---
--- @return string File contents if successfully read, empty string otherwise
---
function Utils.read_file(path)
    local file = io.open(path, 'r')
    if file then
        local content = file:read('*a')
        file:close()
        return content
    end

    return ''
end

--- Generates a file with provided content
---
--- @param path string File path
--- @param content string File content
---
function Utils.gen_file(path, content)
    local file = io.open(path, 'w')
    if file ~= nil then
        file:write(content)
        file:close()
    end
end

--- Recursively copies a table by value
---
--- @param orig any Original table
--- @param seen? any Part of the original table already copied
---
--- @return table Table contents
---
function Utils.copy_table(orig, seen)
    if type(orig) ~= 'table' then
        return orig
    end

    seen = seen or {}
    if seen[orig] then
        return seen[orig] -- handle cycles / shared sub-tables
    end

    local copy = {}
    seen[orig] = copy

    for k, v in pairs(orig) do
        copy[Utils.copy_table(k, seen)] = Utils.copy_table(v, seen)
    end

    return setmetatable(copy, getmetatable(orig))
end

--- Converts string to number if possible
---
--- @param str string String to convert
---
--- @return number | string Number if conversion successful, original string otherwise
---
function Utils.tonumber_if_possible(str)
    return tonumber(str) or str
end

--- Logs message
---
--- Suppresses logs during tests unless explicitly enabled.
---
--- @param msg string Message to log
---
function Utils.log(msg)
    if _G.ZYNC_TESTING and not _G.ZYNC_LOGS then
        return
    end
    print('zync: ' .. msg)
end

--- Normalizes exit code values to a numeric code
---
--- @param code any Exit code value
---
--- @return number Normalized exit code
function Utils.normalize_exit_code(code)
    if code == nil then
        return 0
    end

    if type(code) == 'number' then
        return code
    end

    if type(code) == 'boolean' then
        if code then
            return 0
        end
        return 1
    end

    return 1
end

--- Converts os.execute results to a numeric exit code
---
--- @param ok any               os.execute first return value
--- @param exit_type? string    os.execute exit type
--- @param code? number         os.execute exit code or signal
---
--- @return number Normalized exit code
function Utils.exit_code_from_execute(ok, exit_type, code)
    if type(ok) == 'number' then
        return ok
    end

    if type(ok) == 'boolean' then
        if ok then
            return 0
        end
        if exit_type == 'exit' and type(code) == 'number' then
            return code
        end
        if exit_type == 'signal' and type(code) == 'number' then
            return 128 + code
        end
        return 1
    end

    return 1
end

--- Interpolates string using a table of key-value pairs
---
--- Replaces placeholder keys in a string (${key})
--- with custom values based on the provided table
--- Missing keys are left as-is.
---
--- @param s string                     String to interpolate
--- @param tab table<string, string>    A key-value pair table used for interpolation
---
--- @return string  Interpolated string
---
function Utils.interpolate(s, tab)
    return (s:gsub('($%b{})', function(w)
        return tab[w:sub(3, -2)] or w
    end))
end

local function interpolation_map(source)
    local cache = {}
    local resolving = {}
    local map = {}

    local function resolve(key)
        if cache[key] ~= nil then
            return cache[key]
        end

        if resolving[key] then
            -- Cycle guard: return raw value without further expansion.
            return source[key]
        end

        local value = source[key]
        local value_type = type(value)
        if value_type == 'string' then
            -- Allow nested placeholders to resolve through the map.
            resolving[key] = true
            local resolved = Utils.interpolate(value, map)
            resolving[key] = nil
            cache[key] = resolved
            return resolved
        end
        if value_type == 'number' then
            -- Normalize non-strings to string values for interpolation.
            cache[key] = tostring(value)
            return cache[key]
        end
        if value_type == 'boolean' then
            cache[key] = value and 'true' or 'false'
            return cache[key]
        end

        -- Unknown/non-scalar values are not interpolated.
        return nil
    end

    setmetatable(map, {
        __index = function(_, key)
            return resolve(key)
        end,
    })

    return map
end

--- Recursively interpolates table contents using a source table
---
--- Walks nested tables and replaces string placeholders (${key})
--- with values from the provided source table.
--- Cycles are preserved without recursion.
---
--- @param value any                      Value or table to interpolate
--- @param source table<string, any>      Source table for placeholder values
--- @param seen? table<any, boolean>      Seen tables for cycle detection
--- @param map? table<string, string>     Internal interpolation map
---
--- @return any Interpolated value
---
function Utils.interpolate_recursive(value, source, seen, map)
    source = source or {}
    local value_type = type(value)
    map = map or interpolation_map(source)
    if value_type == 'string' then
        return Utils.interpolate(value, map)
    end
    if value_type ~= 'table' then
        return value
    end

    seen = seen or {}
    if seen[value] then
        return value
    end
    seen[value] = true

    for k, v in pairs(value) do
        value[k] = Utils.interpolate_recursive(v, source, seen, map)
    end

    return value
end

return Utils
