--- Plugin module discovery and canonical path helpers
-- Keeps flat and nested self-name plugin entry layouts equivalent
-- @module core.plugin

local plugin = {}

local reported_layout_conflicts = {}

--- Warn once when both flat and nested plugin entry files exist
-- @param name string plugin short name
-- @param flat_path string flat plugin entry path
-- @param nested_path string nested plugin entry path
-- @param reporter function|nil warning callback
local function report_layout_conflict(name, flat_path, nested_path, reporter)
	if reported_layout_conflicts[name] then
		return
	end

	reported_layout_conflicts[name] = true

	if type(reporter) == "function" then
		reporter(name, flat_path, nested_path)
	end
end

--- Return nested fallback module id for canonical 'plugins.name'
-- @param modname string canonical plugin module id
-- @param plugins_dir string|nil plugins directory
-- @param system_api table|nil filesystem api
-- @return string|nil fallback module id
function plugin.get_nested_fallback_module(modname, plugins_dir, system_api)
	if type(modname) ~= "string" then
		return nil
	end

	local base = modname:match("^plugins%.([%w_]+)$")
	if not base then
		return nil
	end

	system_api = system_api or system

	if type(plugins_dir) ~= "string" then
		return modname .. "." .. base
	end

	local flat_path = system_api.join_path(plugins_dir, base .. ".lua")
	if system_api.is_file(flat_path) == true then
		return nil
	end

	local dir_path = system_api.join_path(plugins_dir, base)
	local nested_path = system_api.join_path(dir_path, base .. ".lua")
	if system_api.is_file(nested_path) == true then
		return modname .. "." .. base
	end

	return nil
end

--- Require plugin module by canonical id with nested-file fallback
-- @param modname string canonical plugin module id
-- @param plugins_dir string|nil plugins directory
-- @param require_fn function|nil module require function
-- @param system_api table|nil filesystem api
-- @return boolean ok
-- @return any result_or_error
function plugin.require_module(modname, plugins_dir, require_fn, system_api)
	require_fn = require_fn or require
	local ok, result = pcall(require_fn, modname)
	if ok then
		return true, result
	end

	local fallback = plugin.get_nested_fallback_module(modname, plugins_dir, system_api)
	if not fallback then
		return false, result
	end

	return pcall(require_fn, fallback)
end

--- Discover canonical plugin module ids from flat and nested entry files
-- @param plugins_dir string|nil plugins directory
-- @param system_api table|nil filesystem api
-- @param reporter function|nil warning callback
-- @return table module ids
function plugin.get_modules(plugins_dir, system_api, reporter)
	system_api = system_api or system

	if type(plugins_dir) ~= "string" then
		return {}
	end

	local files = system_api.list_dir(plugins_dir) or {}
	local modules = {}
	local flat_entries = {}
	local seen_modules = {}

	local function add_module_name(name)
		if type(name) ~= "string" or name == "" then
			return
		end

		if seen_modules[name] then
			return
		end

		seen_modules[name] = true
		modules[#modules + 1] = "plugins." .. name
	end

	-- Discover canonical 'plugins.name' from 'data/plugins/name.lua'
	for _, filename in ipairs(files) do
		if filename:match("%.lua$") then
			local name = filename:gsub("%.lua$", "")
			flat_entries[name] = true
			add_module_name(name)
		end
	end

	-- Discover canonical 'plugins.name' from 'data/plugins/name/name.lua'
	for _, entry in ipairs(files) do
		local dir_path = system_api.join_path(plugins_dir, entry)
		local info = system_api.get_file_info(dir_path)

		if info and info.type == "dir" then
			local nested = system_api.join_path(dir_path, entry .. ".lua")

			if system_api.is_file(nested) == true then
				if flat_entries[entry] then
					local flat_path = system_api.join_path(plugins_dir, entry .. ".lua")
					report_layout_conflict(entry, flat_path, nested, reporter)
				else
					add_module_name(entry)
				end
			end
		end
	end

	table.sort(modules)

	return modules
end

--- Return the real source path for one plugin module when it exists
-- @param modname string plugin module id
-- @param plugins_dir string|nil plugins directory
-- @param system_api table|nil filesystem api
-- @return string|nil source path
function plugin.get_module_path(modname, plugins_dir, system_api)
	if type(modname) ~= "string" or type(plugins_dir) ~= "string" then
		return nil
	end

	system_api = system_api or system

	local rel = modname:match("^plugins%.(.+)$")
	if not rel then
		return nil
	end

	local flat_path = system_api.join_path(plugins_dir, rel:gsub("%.", "/") .. ".lua")
	if system_api.is_file(flat_path) == true then
		return flat_path
	end

	local fallback = plugin.get_nested_fallback_module(modname, plugins_dir, system_api)
	if not fallback then
		return nil
	end

	local nested_rel = fallback:match("^plugins%.(.+)$")
	if not nested_rel then
		return nil
	end

	local nested_path = system_api.join_path(plugins_dir, nested_rel:gsub("%.", "/") .. ".lua")
	if system_api.is_file(nested_path) == true then
		return nested_path
	end

	return nil
end

--- Return the optional translations module for one canonical plugin id
-- Translation files live at 'plugins/name/translations.lua' for flat and nested entries
-- @param modname string canonical plugin module id
-- @param plugins_dir string|nil plugins directory
-- @param system_api table|nil filesystem api
-- @param package_api table|nil Lua package table
-- @return string|nil module_name translations module id when present
function plugin.get_translations_module(modname, plugins_dir, system_api, package_api)
	if type(modname) ~= "string" or type(plugins_dir) ~= "string" then
		return nil
	end

	local name = modname:match("^plugins%.([%w_]+)$")
	if not name then
		return nil
	end

	local module_name = "plugins." .. name .. ".translations"
	package_api = package_api or package
	if package_api.preload and package_api.preload[module_name] then
		return module_name
	end

	system_api = system_api or system
	local plugin_dir = system_api.join_path(plugins_dir, name)
	local path = system_api.join_path(plugin_dir, "translations.lua")
	if system_api.is_file(path) ~= true then
		return nil
	end

	return module_name
end

--- Return the translation catalog owner for one canonical plugin id
-- @param modname string canonical plugin module id
-- @return string|nil owner translation owner id
function plugin.get_translation_owner(modname)
	local name = type(modname) == "string" and modname:match("^plugins%.([%w_]+)$") or nil

	return name and ("plugin." .. name) or nil
end

return plugin
