using System.Text.Json;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
using YMhut.Box.Core.App;

namespace YMhut.Box.Core.Data;

public sealed record ReferenceDataValidationResult(
    IReadOnlyList<string> PresentPaths,
    IReadOnlyList<string> MissingPaths)
{
    public bool IsHealthy => MissingPaths.Count == 0;
}

public interface IReferenceDataService
{
    Task<string?> ReadTextAsync(string relativePath, CancellationToken cancellationToken = default);

    Task<T?> ReadJsonAsync<T>(string relativePath, CancellationToken cancellationToken = default);

    bool Exists(string relativePath);

    IReadOnlyList<string> EnumerateFiles(string relativeDirectory, string searchPattern = "*.*");

    ReferenceDataValidationResult ValidateRequiredAssets(params string[] relativePaths);
}

public sealed class ReferenceDataService(AppPaths paths) : IReferenceDataService
{
    private static readonly byte[] YbinMagic = Encoding.ASCII.GetBytes("YMHUTYBIN1");
    private static readonly byte[] YbinKey = SHA256.HashData(Encoding.UTF8.GetBytes("YMhut.Box.ReferenceData.YBin.v1"));

    public async Task<string?> ReadTextAsync(string relativePath, CancellationToken cancellationToken = default)
    {
        var fullPath = ResolveAssetPath(relativePath);
        if (File.Exists(fullPath))
        {
            return await File.ReadAllTextAsync(fullPath, cancellationToken).ConfigureAwait(false);
        }

        return await ReadFromDatAsync(relativePath, cancellationToken).ConfigureAwait(false);
    }

    public async Task<T?> ReadJsonAsync<T>(string relativePath, CancellationToken cancellationToken = default)
    {
        var text = await ReadTextAsync(relativePath, cancellationToken).ConfigureAwait(false);
        return string.IsNullOrWhiteSpace(text)
            ? default
            : JsonSerializer.Deserialize<T>(text, new JsonSerializerOptions(JsonSerializerDefaults.Web));
    }

    public bool Exists(string relativePath)
    {
        return File.Exists(ResolveAssetPath(relativePath)) || DatContains(relativePath);
    }

    public IReadOnlyList<string> EnumerateFiles(string relativeDirectory, string searchPattern = "*.*")
    {
        var directory = ResolveAssetDirectory(relativeDirectory);
        var physicalFiles = Directory.Exists(directory)
            ? Directory.EnumerateFiles(directory, searchPattern, SearchOption.AllDirectories)
                .Select(path => Path.GetRelativePath(directory, path))
                .Order(StringComparer.OrdinalIgnoreCase)
                .ToArray()
            : [];
        var datFiles = EnumerateDatFiles(relativeDirectory, searchPattern);
        return physicalFiles
            .Concat(datFiles)
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .Order(StringComparer.OrdinalIgnoreCase)
            .ToArray();
    }

    public ReferenceDataValidationResult ValidateRequiredAssets(params string[] relativePaths)
    {
        var present = new List<string>();
        var missing = new List<string>();
        foreach (var path in relativePaths)
        {
            if (Exists(path))
            {
                present.Add(path);
            }
            else
            {
                missing.Add(path);
            }
        }

        return new ReferenceDataValidationResult(present, missing);
    }

    private string ResolveAssetPath(string relativePath)
    {
        var normalized = relativePath.Replace('/', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar);
        var assetPath = Path.Combine(paths.Assets, normalized);
        if (File.Exists(assetPath))
        {
            return assetPath;
        }

        return Path.Combine(AppContext.BaseDirectory, normalized);
    }

    private string ResolveAssetDirectory(string relativeDirectory)
    {
        var normalized = relativeDirectory.Replace('/', Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar);
        var assetDirectory = Path.Combine(paths.Assets, normalized);
        if (Directory.Exists(assetDirectory))
        {
            return assetDirectory;
        }

        return Path.Combine(AppContext.BaseDirectory, normalized);
    }

    private async Task<string?> ReadFromDatAsync(string relativePath, CancellationToken cancellationToken)
    {
        var entryName = NormalizeEntryName(relativePath);
        foreach (var package in DatCandidates())
        {
            if (!File.Exists(package))
            {
                continue;
            }

            using var archive = OpenPackage(package);
            var entry = archive.GetEntry(entryName);
            if (entry is null)
            {
                continue;
            }

            await using var stream = entry.Open();
            using var reader = new StreamReader(stream);
            return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
        }

        return null;
    }

    private bool DatContains(string relativePath)
    {
        var entryName = NormalizeEntryName(relativePath);
        foreach (var package in DatCandidates())
        {
            if (!File.Exists(package))
            {
                continue;
            }

            using var archive = OpenPackage(package);
            if (archive.GetEntry(entryName) is not null)
            {
                return true;
            }
        }

        return false;
    }

    private IReadOnlyList<string> EnumerateDatFiles(string relativeDirectory, string searchPattern)
    {
        var prefix = NormalizeEntryName(relativeDirectory).TrimEnd('/') + "/";
        var extension = searchPattern.StartsWith("*.") ? searchPattern[1..] : string.Empty;
        var results = new List<string>();
        foreach (var package in DatCandidates())
        {
            if (!File.Exists(package))
            {
                continue;
            }

            using var archive = OpenPackage(package);
            results.AddRange(archive.Entries
                .Where(entry => entry.FullName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                .Where(entry => string.IsNullOrWhiteSpace(extension) || entry.FullName.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
                .Select(entry => entry.FullName[prefix.Length..]));
        }

        return results;
    }

    private IEnumerable<string> DatCandidates()
    {
        yield return Path.Combine(paths.Assets, "data", "ymhut-data.ybin");
        yield return Path.Combine(AppContext.BaseDirectory, "Assets", "data", "ymhut-data.ybin");
        yield return Path.Combine(AppContext.BaseDirectory, "resources", "ymhut-data.ybin");
        yield return Path.Combine(paths.Assets, "data", "reference-data.dat");
        yield return Path.Combine(AppContext.BaseDirectory, "Assets", "data", "reference-data.dat");
        yield return Path.Combine(AppContext.BaseDirectory, "resources", "reference-data.dat");
    }

    private static string NormalizeEntryName(string relativePath)
    {
        return relativePath.Replace('\\', '/').TrimStart('/');
    }

    private static ZipArchive OpenPackage(string package)
    {
        if (package.EndsWith(".ybin", StringComparison.OrdinalIgnoreCase))
        {
            var encrypted = File.ReadAllBytes(package);
            if (encrypted.Length <= YbinMagic.Length + 16 ||
                !encrypted.AsSpan(0, YbinMagic.Length).SequenceEqual(YbinMagic))
            {
                throw new InvalidDataException("Reference data package has an invalid header.");
            }

            var iv = encrypted.AsSpan(YbinMagic.Length, 16).ToArray();
            var cipherText = encrypted.AsSpan(YbinMagic.Length + 16).ToArray();
            using var aes = Aes.Create();
            aes.Key = YbinKey;
            aes.IV = iv;
            aes.Mode = CipherMode.CBC;
            aes.Padding = PaddingMode.PKCS7;
            using var decryptor = aes.CreateDecryptor();
            var payload = decryptor.TransformFinalBlock(cipherText, 0, cipherText.Length);
            return new ZipArchive(new MemoryStream(payload), ZipArchiveMode.Read, leaveOpen: false);
        }

        return ZipFile.OpenRead(package);
    }
}
