using System.Text.Json;
using System.Text.Json.Serialization;

namespace Metin2Launcher.Runtime;

/// <summary>
/// In-memory representation of a runtime key bundle delivered alongside an
/// m2pack release. The launcher never decrypts or opens .m2p archives itself;
/// it merely parses the key bundle produced by the release tool and passes it
/// through to the game executable via one of the <see cref="IRuntimeKeyDelivery"/>
/// mechanisms.
///
/// Tolerant of two JSON shapes seen in the wild:
///
/// 1. Native launcher shape (canonical):
/// <code>
/// {
///   "key_id": "2026.04.14-1",
///   "master_key_hex": "&lt;64 hex chars&gt;",
///   "sign_pubkey_hex": "&lt;64 hex chars&gt;"
/// }
/// </code>
///
/// 2. <c>m2pack export-runtime-key --format json</c> output, where
///    <c>key_id</c> is emitted as an integer and the public key field is
///    named <c>sign_public_key_hex</c>. Extra fields like <c>version</c> and
///    <c>mapping_name</c> are ignored. See
///    <c>metin-server/m2pack-secure#3</c>.
/// </summary>
public sealed class RuntimeKey
{
    public string KeyId { get; set; } = "";
    public string MasterKeyHex { get; set; } = "";
    public string SignPubkeyHex { get; set; } = "";

    /// <summary>Parses a runtime-key.json blob. Throws on malformed or incomplete data.</summary>
    public static RuntimeKey Parse(ReadOnlySpan<byte> raw)
    {
        // Go through JsonSerializer.Deserialize&lt;JsonElement&gt; rather than
        // JsonDocument.Parse so malformed input throws JsonException (as the
        // old path did) rather than its internal JsonReaderException subtype,
        // which Assert.Throws&lt;JsonException&gt; in the tests won't match.
        var root = JsonSerializer.Deserialize<JsonElement>(raw);
        if (root.ValueKind != JsonValueKind.Object)
            throw new InvalidDataException("runtime-key root must be a JSON object");

        var rk = new RuntimeKey
        {
            KeyId = ReadKeyId(root),
            MasterKeyHex = ReadString(root, "master_key_hex"),
            SignPubkeyHex = ReadString(root, "sign_pubkey_hex", "sign_public_key_hex"),
        };
        Validate(rk);
        return rk;
    }

    // key_id can be a plain string or an integer (m2pack CLI emits it as an
    // int). Stringify numeric variants so callers always see a string.
    private static string ReadKeyId(JsonElement root)
    {
        if (!root.TryGetProperty("key_id", out var el))
            return "";
        return el.ValueKind switch
        {
            JsonValueKind.String => el.GetString() ?? "",
            JsonValueKind.Number => el.TryGetInt64(out var n) ? n.ToString() : el.GetRawText(),
            _ => "",
        };
    }

    private static string ReadString(JsonElement root, params string[] names)
    {
        foreach (var name in names)
        {
            if (root.TryGetProperty(name, out var el) && el.ValueKind == JsonValueKind.String)
                return el.GetString() ?? "";
        }
        return "";
    }

    public static RuntimeKey Load(string path)
    {
        var bytes = File.ReadAllBytes(path);
        return Parse(bytes);
    }

    private static void Validate(RuntimeKey rk)
    {
        if (string.IsNullOrWhiteSpace(rk.KeyId))
            throw new InvalidDataException("runtime-key missing 'key_id'");
        if (!IsHex(rk.MasterKeyHex, 64))
            throw new InvalidDataException("runtime-key 'master_key_hex' must be 64 hex chars");
        if (!IsHex(rk.SignPubkeyHex, 64))
            throw new InvalidDataException("runtime-key 'sign_pubkey_hex' must be 64 hex chars");
    }

    private static bool IsHex(string s, int expectedLen)
    {
        if (s is null || s.Length != expectedLen) return false;
        for (int i = 0; i < s.Length; i++)
        {
            char c = s[i];
            bool ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
            if (!ok) return false;
        }
        return true;
    }
}
