using YMhut.Box.Core.App;
using YMhut.Box.Core.Logging;
using YMhut.Box.Core.Settings;
using System.Security.Cryptography;
using System.Text;

namespace YMhut.Box.Core.Plugins;

public interface IBuiltInPluginInstallerService
{
    Task EnsureInstalledAsync(CancellationToken cancellationToken = default);
}

public sealed class BuiltInPluginInstallerService(
    AppPaths paths,
    ILogService? logService = null,
    ISettingsService? settingsService = null) : IBuiltInPluginInstallerService
{
    private const string ResourcePrefix = "YMhut.Box.Core.Plugins.BuiltIn.";
    private const string FingerprintFileName = ".ymhut-built-in.sha256";
    private static readonly string[] KnownRootFiles =
    [
        "README.md",
        "ymhut.plugin.json",
        "index.html",
        "style.css",
        "main.js"
    ];
    private readonly SemaphoreSlim _gate = new(1, 1);

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

    public async Task EnsureInstalledAsync(CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            Directory.CreateDirectory(PluginsRoot);
            var plugins = DiscoverEmbeddedPlugins();
            if (plugins.Count == 0)
            {
                await WriteLogAsync("Warning", "No built-in plugin resources found", PluginsRoot, cancellationToken).ConfigureAwait(false);
                return;
            }

            foreach (var plugin in plugins)
            {
                var targetRoot = Path.Combine(PluginsRoot, plugin.FolderName);
                if (Directory.Exists(targetRoot))
                {
                    if (await IsUnmodifiedBuiltInPluginAsync(targetRoot, cancellationToken).ConfigureAwait(false))
                    {
                        Directory.Delete(targetRoot, recursive: true);
                        await ExtractEmbeddedPluginAsync(plugin, targetRoot, cancellationToken).ConfigureAwait(false);
                        await WriteLogAsync("Information", "Built-in plugin refreshed", targetRoot, cancellationToken).ConfigureAwait(false);
                    }

                    continue;
                }

                await ExtractEmbeddedPluginAsync(plugin, targetRoot, cancellationToken).ConfigureAwait(false);
                await WriteLogAsync("Information", "Built-in plugin installed", targetRoot, cancellationToken).ConfigureAwait(false);
            }
        }
        finally
        {
            _gate.Release();
        }
    }

    private static async Task<bool> IsUnmodifiedBuiltInPluginAsync(string targetRoot, CancellationToken cancellationToken)
    {
        var fingerprintPath = Path.Combine(targetRoot, FingerprintFileName);
        if (!File.Exists(fingerprintPath))
        {
            return false;
        }

        try
        {
            var saved = (await File.ReadAllTextAsync(fingerprintPath, cancellationToken).ConfigureAwait(false)).Trim();
            if (string.IsNullOrWhiteSpace(saved))
            {
                return false;
            }

            var current = await ComputeDirectoryFingerprintAsync(targetRoot, cancellationToken).ConfigureAwait(false);
            return string.Equals(saved, current, StringComparison.OrdinalIgnoreCase);
        }
        catch (IOException)
        {
            return false;
        }
        catch (UnauthorizedAccessException)
        {
            return false;
        }
    }

    private static async Task ExtractEmbeddedPluginAsync(EmbeddedBuiltInPlugin plugin, string targetRoot, CancellationToken cancellationToken)
    {
        var assembly = typeof(BuiltInPluginInstallerService).Assembly;
        Directory.CreateDirectory(targetRoot);
        foreach (var resource in plugin.Resources)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var target = Path.GetFullPath(Path.Combine(targetRoot, resource.RelativePath));
            if (!PluginRegistryService.IsInside(targetRoot, target))
            {
                throw new InvalidDataException($"Built-in plugin resource escapes target directory: {resource.ResourceName}");
            }

            Directory.CreateDirectory(Path.GetDirectoryName(target)!);
            await using var input = assembly.GetManifestResourceStream(resource.ResourceName)
                ?? throw new InvalidOperationException($"Built-in plugin resource cannot be opened: {resource.ResourceName}");
            await using var output = File.Create(target);
            await input.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
        }

        var fingerprint = await ComputeDirectoryFingerprintAsync(targetRoot, cancellationToken).ConfigureAwait(false);
        await File.WriteAllTextAsync(Path.Combine(targetRoot, FingerprintFileName), fingerprint, Encoding.UTF8, cancellationToken).ConfigureAwait(false);
    }

    private static async Task<string> ComputeDirectoryFingerprintAsync(string targetRoot, CancellationToken cancellationToken)
    {
        using var sha = SHA256.Create();
        var files = Directory.EnumerateFiles(targetRoot, "*", SearchOption.AllDirectories)
            .Where(path => !string.Equals(Path.GetFileName(path), FingerprintFileName, StringComparison.OrdinalIgnoreCase))
            .Select(path => Path.GetFullPath(path))
            .Order(StringComparer.OrdinalIgnoreCase);
        foreach (var file in files)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var relative = file.Substring(Path.GetFullPath(targetRoot).Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar).Replace('\\', '/');
            var nameBytes = Encoding.UTF8.GetBytes(relative);
            sha.TransformBlock(nameBytes, 0, nameBytes.Length, null, 0);
            sha.TransformBlock([0], 0, 1, null, 0);
            var content = await File.ReadAllBytesAsync(file, cancellationToken).ConfigureAwait(false);
            sha.TransformBlock(content, 0, content.Length, null, 0);
            sha.TransformBlock([0], 0, 1, null, 0);
        }

        sha.TransformFinalBlock([], 0, 0);
        return Convert.ToHexString(sha.Hash ?? []);
    }

    private Task WriteLogAsync(string level, string message, string detail, CancellationToken cancellationToken)
    {
        return logService?.WriteAsync(level, "plugin", message, detail, cancellationToken) ?? Task.CompletedTask;
    }

    private static IReadOnlyList<EmbeddedBuiltInPlugin> DiscoverEmbeddedPlugins()
    {
        var assembly = typeof(BuiltInPluginInstallerService).Assembly;
        var resources = assembly.GetManifestResourceNames()
            .Where(name => name.StartsWith(ResourcePrefix, StringComparison.Ordinal))
            .Select(ParseResource)
            .OfType<EmbeddedBuiltInResource>()
            .GroupBy(resource => resource.PluginToken, StringComparer.OrdinalIgnoreCase)
            .Select(group => new EmbeddedBuiltInPlugin(
                DecodePluginFolder(group.Key),
                group.OrderBy(resource => resource.RelativePath, StringComparer.OrdinalIgnoreCase).ToArray()))
            .Where(plugin => plugin.Resources.Any(resource => string.Equals(resource.RelativePath, PluginManifest.FileName, StringComparison.OrdinalIgnoreCase)))
            .OrderBy(plugin => plugin.FolderName, StringComparer.OrdinalIgnoreCase)
            .ToArray();

        return resources;
    }

    private static EmbeddedBuiltInResource? ParseResource(string resourceName)
    {
        var relativeName = resourceName[ResourcePrefix.Length..];
        foreach (var fileName in KnownRootFiles)
        {
            var suffix = "." + fileName;
            if (!relativeName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
            {
                continue;
            }

            var token = relativeName[..^suffix.Length];
            if (string.IsNullOrWhiteSpace(token))
            {
                return null;
            }

            return new EmbeddedBuiltInResource(token, fileName, resourceName);
        }

        return null;
    }

    private static string DecodePluginFolder(string token)
    {
        return token.Replace('_', '-');
    }

    private sealed record EmbeddedBuiltInPlugin(
        string FolderName,
        IReadOnlyList<EmbeddedBuiltInResource> Resources);

    private sealed record EmbeddedBuiltInResource(
        string PluginToken,
        string RelativePath,
        string ResourceName);
}
