using System.Text.Json;

namespace Metin2Launcher.Manifest;

/// <summary>
/// Fetches a manifest (and its signature) over HTTP and parses it.
///
/// Important invariant: we keep the raw byte slice of the manifest body so the
/// signature verifier can operate on the exact bytes the server delivered.
/// Never re-serialize before verifying — the signature is over literal bytes.
/// </summary>
public sealed class ManifestLoader
{
    private readonly HttpClient _http;

    public ManifestLoader(HttpClient http)
    {
        _http = http;
    }

    public static JsonSerializerOptions JsonOptions { get; } = new()
    {
        PropertyNameCaseInsensitive = false,
        WriteIndented = true,
        // Field order in our DTOs is already source-ordered; serializer writes them in declaration order.
    };

    /// <summary>
    /// Downloads manifest.json and manifest.json.sig, returns raw bytes and parsed DTO.
    /// The caller is responsible for verifying the signature before trusting the parsed manifest.
    /// </summary>
    public async Task<LoadedManifest> FetchAsync(
        string manifestUrl,
        string signatureUrl,
        CancellationToken ct)
    {
        var manifestBytes = await GetBytesAsync(manifestUrl, ct).ConfigureAwait(false);
        var sigBytes      = await GetBytesAsync(signatureUrl, ct).ConfigureAwait(false);

        var manifest = Parse(manifestBytes);
        return new LoadedManifest(manifestBytes, sigBytes, manifest);
    }

    /// <summary>Parses a manifest from raw bytes. Throws on malformed JSON.</summary>
    public static Manifest Parse(ReadOnlySpan<byte> raw)
    {
        var m = JsonSerializer.Deserialize<Manifest>(raw, JsonOptions);
        if (m is null)
            throw new InvalidDataException("Manifest JSON deserialized to null");
        if (string.IsNullOrEmpty(m.Version))
            throw new InvalidDataException("Manifest is missing required field 'version'");
        if (m.Launcher is null || string.IsNullOrEmpty(m.Launcher.Sha256))
            throw new InvalidDataException("Manifest is missing required field 'launcher'");
        m.Files ??= new List<ManifestFile>();
        return m;
    }

    /// <summary>Serializes a manifest back to bytes — used only in tests for round-trip checks.</summary>
    public static byte[] Serialize(Manifest manifest)
        => JsonSerializer.SerializeToUtf8Bytes(manifest, JsonOptions);

    private async Task<byte[]> GetBytesAsync(string url, CancellationToken ct)
    {
        using var resp = await _http.GetAsync(url, HttpCompletionOption.ResponseContentRead, ct)
            .ConfigureAwait(false);
        resp.EnsureSuccessStatusCode();
        return await resp.Content.ReadAsByteArrayAsync(ct).ConfigureAwait(false);
    }
}

public sealed record LoadedManifest(byte[] RawBytes, byte[] Signature, Manifest Manifest);
