using System.Text.Json;
using System.Text.Json.Serialization;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Settings;
using YMhut.Box.Core.Tools;

namespace YMhut.Box.Core.Plugins;

public interface IPluginRegistryService
{
    string PluginsRoot { get; }

    Task<IReadOnlyList<LoadedPlugin>> LoadPluginsAsync(CancellationToken cancellationToken = default);

    Task<IReadOnlyList<PluginToolModule>> LoadEnabledToolModulesAsync(CancellationToken cancellationToken = default);
}

public sealed class PluginRegistryService(
    AppPaths paths,
    IPluginStateStore stateStore,
    ILogService? logService = null,
    ISettingsService? settingsService = null,
    IBuiltInPluginInstallerService? builtInInstaller = null) : IPluginRegistryService
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        PropertyNameCaseInsensitive = true,
        Converters = { new JsonStringEnumConverter<PluginPermission>(), new JsonStringEnumConverter<PluginSurfaceKind>() }
    };

    public string PluginsRoot => ResolvePluginsRoot(paths, settingsService?.Current.PluginRootPath);

    public static string DefaultPluginsRoot(AppPaths paths) => Path.Combine(paths.Root, "Plugins");

    public static string ResolvePluginsRoot(AppPaths paths, string? configuredPath)
    {
        return string.IsNullOrWhiteSpace(configuredPath)
            ? DefaultPluginsRoot(paths)
            : Path.GetFullPath(Environment.ExpandEnvironmentVariables(configuredPath.Trim()));
    }

    public async Task<IReadOnlyList<LoadedPlugin>> LoadPluginsAsync(CancellationToken cancellationToken = default)
    {
        if (settingsService is not null && !settingsService.Current.PluginsEnabled)
        {
            return [];
        }

        if (builtInInstaller is not null)
        {
            await builtInInstaller.EnsureInstalledAsync(cancellationToken).ConfigureAwait(false);
        }

        Directory.CreateDirectory(PluginsRoot);
        var plugins = new List<LoadedPlugin>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var builtInIds = ToolCatalog.DefaultModules().Select(module => module.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);

        foreach (var directory in Directory.EnumerateDirectories(PluginsRoot).Order(StringComparer.OrdinalIgnoreCase))
        {
            var manifestPath = Path.Combine(directory, PluginManifest.FileName);
            var errors = new List<string>();
            PluginManifest? manifest = null;
            try
            {
                if (!File.Exists(manifestPath))
                {
                    errors.Add("Missing ymhut.plugin.json.");
                }
                else
                {
                    await using var stream = File.OpenRead(manifestPath);
                    manifest = await JsonSerializer.DeserializeAsync<PluginManifest>(stream, JsonOptions, cancellationToken).ConfigureAwait(false);
                    if (manifest is null)
                    {
                        errors.Add("Manifest is empty or invalid.");
                    }
                }
            }
            catch (Exception exception) when (exception is JsonException or IOException or UnauthorizedAccessException)
            {
                errors.Add(exception.Message);
            }

            manifest ??= new PluginManifest(Path.GetFileName(directory), Path.GetFileName(directory), "0.0.0", string.Empty, string.Empty, string.Empty, [], [], []);
            ValidateManifest(manifest, directory, seen, builtInIds, errors);
            seen.Add(manifest.Id);

            var state = await stateStore.GetStateAsync(manifest.Id, cancellationToken).ConfigureAwait(false);
            var loaded = new LoadedPlugin(manifest, directory, state, errors);
            plugins.Add(loaded);
            if (errors.Count > 0)
            {
                await (logService?.WriteAsync("Warning", $"plugin:{manifest.Id}", "Plugin validation failed", string.Join(Environment.NewLine, errors), cancellationToken) ?? Task.CompletedTask)
                    .ConfigureAwait(false);
            }
        }

        return plugins;
    }

    public async Task<IReadOnlyList<PluginToolModule>> LoadEnabledToolModulesAsync(CancellationToken cancellationToken = default)
    {
        if (settingsService is not null && !settingsService.Current.PluginsEnabled)
        {
            return [];
        }

        var plugins = await LoadPluginsAsync(cancellationToken).ConfigureAwait(false);
        return plugins
            .Where(plugin => plugin.IsValid && plugin.State.Enabled)
            .SelectMany(plugin => plugin.Manifest.Surfaces
                .Where(surface => surface.Kind == PluginSurfaceKind.ToolboxTool &&
                    (plugin.State.MountedSurfaceIds.Count == 0 || plugin.State.MountedSurfaceIds.Contains(surface.Id)))
                .Select(surface => new PluginToolModule(plugin, surface)))
            .ToArray();
    }

    private static void ValidateManifest(
        PluginManifest manifest,
        string root,
        ISet<string> seen,
        ISet<string> builtInIds,
        IList<string> errors)
    {
        if (!PluginIds.IsSafeId(manifest.Id) || PluginIds.IsPluginToolId(manifest.Id))
        {
            errors.Add("Plugin id must be a safe local id and cannot start with plugin:.");
        }
        if (seen.Contains(manifest.Id))
        {
            errors.Add("Duplicate plugin id.");
        }
        if (string.IsNullOrWhiteSpace(manifest.Name))
        {
            errors.Add("Plugin name is required.");
        }
        if (string.IsNullOrWhiteSpace(manifest.Version))
        {
            errors.Add("Plugin version is required.");
        }
        if (!IsSafeRelativeFile(root, manifest.Entry))
        {
            errors.Add("Plugin entry must point to a file inside the plugin directory.");
        }
        if (!HasReadme(root))
        {
            errors.Add("Plugin package must include README.md, README.txt, or 说明.md.");
        }

        var surfaceIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        foreach (var surface in manifest.Surfaces)
        {
            if (!PluginIds.IsSafeId(surface.Id))
            {
                errors.Add($"Surface id is invalid: {surface.Id}");
            }
            if (!surfaceIds.Add(surface.Id))
            {
                errors.Add($"Duplicate surface id: {surface.Id}");
            }
            if (builtInIds.Contains(surface.Id) || builtInIds.Contains(PluginIds.ToolId(manifest.Id, surface.Id)))
            {
                errors.Add($"Surface id conflicts with a built-in tool: {surface.Id}");
            }
            if (!IsSafeRelativeFile(root, surface.EffectiveEntry(manifest.Entry)))
            {
                errors.Add($"Surface entry must point to a file inside the plugin directory: {surface.Id}");
            }
        }

        foreach (var resource in manifest.Resources)
        {
            if (!IsSafeRelativePath(root, resource))
            {
                errors.Add($"Resource path escapes plugin directory: {resource}");
            }
            if (resource.Contains("Assets/icons", StringComparison.OrdinalIgnoreCase) ||
                resource.Contains("developer", StringComparison.OrdinalIgnoreCase) ||
                resource.Contains("about", StringComparison.OrdinalIgnoreCase))
            {
                errors.Add($"Core asset override is not allowed: {resource}");
            }
        }
    }

    public static bool IsSafeRelativeFile(string root, string relativePath)
    {
        if (string.IsNullOrWhiteSpace(relativePath))
        {
            return false;
        }

        var fullPath = Path.GetFullPath(Path.Combine(root, relativePath));
        return IsInside(root, fullPath) && File.Exists(fullPath);
    }

    public static bool IsSafeRelativePath(string root, string relativePath)
    {
        if (string.IsNullOrWhiteSpace(relativePath))
        {
            return false;
        }

        var fullPath = Path.GetFullPath(Path.Combine(root, relativePath));
        return IsInside(root, fullPath);
    }

    public static bool IsInside(string root, string path)
    {
        var normalizedRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
        var normalizedPath = Path.GetFullPath(path);
        return normalizedPath.StartsWith(normalizedRoot, StringComparison.OrdinalIgnoreCase);
    }

    private static bool HasReadme(string root)
    {
        return File.Exists(Path.Combine(root, "README.md")) ||
            File.Exists(Path.Combine(root, "README.txt")) ||
            File.Exists(Path.Combine(root, "说明.md"));
    }
}
