local M = {}
local fs = require("luarocks.fs")
local path = require("luarocks.path")
local dir = require("luarocks.dir")

---@param base string
---@param ext string
---@return string[]
local function collect_files(base, ext)
    local files = {}
    local function walk(current)
        if fs.is_dir(current) then
            for _, entry in ipairs(fs.list_dir(current) or {}) do
                walk(dir.path(current, entry))
            end
        elseif ext == "" or current:sub(-#ext) == ext then
            files[#files + 1] = current
        end
    end
    walk(base)
    return files
end

---@param src string
---@param dst_dir string
---@param strip boolean?
---@return true?, string?
local function flatten_move(src, dst_dir, strip)
    local basename = dir.base_name(src)
    if strip and basename:sub(-4) == ".lua" then
        basename = basename:sub(1, -5)
    end
    local dst = dir.path(dst_dir, basename)
    local ok, err = fs.move(src, dst)
    if ok then
        return true, dst
    end
    return nil, err
end

---@param base_dir string
---@return true?, string?
local function extract_tds_zips(base_dir)
    if not fs.is_dir(base_dir) then
        return true
    end

    local entries = fs.list_dir(base_dir) or {}
    for _, entry in ipairs(entries) do
        if entry:sub(-8) == ".tds.zip" then
            local zip_path = dir.path(base_dir, entry)
            local ok, err = fs.unzip(zip_path)
            if not ok then
                return nil, "failed to unzip " .. zip_path .. ": " .. tostring(err)
            end
        end
    end

    return true
end

---Check if file starts with a shebang line.
---@param filepath string
---@return boolean
local function has_shebang(filepath)
    local f = io.open(filepath, "r")
    if not f then
        return false
    end
    local first_line = f:read("*l")
    f:close()
    if not first_line then
        return false
    end
    return first_line:sub(1, 2) == "#!"
end

---core function
---@param rockspec table
---@param no_install boolean?
---@return true?, string?
function M.run(rockspec, no_install)
    -- 通过 rockspec.build.tds.extract 控制是否解压，默认 true
    local extract_tds = false
    if rockspec.build
        and rockspec.build.tds
        and rockspec.build.tds.extract ~= nil
    then
        extract_tds = rockspec.build.tds.extract
    end

    if extract_tds then
        local ok, err = extract_tds_zips(".")
        if not ok then
            return nil, err
        end
    end

    local scripts_dir = dir.path(rockspec.source.dir, "scripts")

    if fs.is_dir(scripts_dir) then
        local script_files = collect_files(scripts_dir, "")

        if #script_files > 0 then
            local bin_dir = path.bin_dir(rockspec.name, rockspec.version)
            if not bin_dir then
                return nil, "failed to resolve bin_dir for " .. rockspec.name
            end

            local lua_dir = path.lua_dir(rockspec.name, rockspec.version)
            if not lua_dir then
                return nil, "failed to resolve lua_dir for " .. rockspec.name
            end

            if not no_install then
                fs.make_dir(bin_dir)
                fs.make_dir(lua_dir)
            end

            for _, src in ipairs(script_files) do
                if no_install then
                    if not fs.exists(src) then
                        return nil, "source script does not exist: " .. src
                    end
                else
                    if has_shebang(src) then
                        -- shebang → bin_dir, strip .lua extension
                        local ok, err = flatten_move(src, bin_dir, true)
                        if not ok then
                            return nil, "failed to move script " .. src .. ": " .. tostring(err)
                        end
                    elseif src:sub(-4) == ".lua" then
                        -- .lua without shebang → lua_dir, keep original name
                        local ok, err = flatten_move(src, lua_dir)
                        if not ok then
                            return nil, "failed to move script " .. src .. ": " .. tostring(err)
                        end
                    end
                    -- other files: skip
                end
            end
        end
    end

    local tex_dir = dir.path(rockspec.source.dir, "tex")

    if fs.is_dir(tex_dir) then
        local lua_files = collect_files(tex_dir, ".lua")

        if #lua_files > 0 then
            local lua_dir = path.lua_dir(rockspec.name, rockspec.version)
            if not lua_dir then
                return nil, "failed to resolve lua_dir for " .. rockspec.name
            end

            if not no_install then
                fs.make_dir(lua_dir)
            end

            for _, src in ipairs(lua_files) do
                if no_install then
                    if not fs.exists(src) then
                        return nil, "source file does not exist: " .. src
                    end
                else
                    local ok, err = flatten_move(src, lua_dir)
                    if not ok then
                        return nil, "failed to move " .. src .. ": " .. tostring(err)
                    end
                end
            end
        end
    end

    return true
end

return M
