// =============================================================================
// Tesla.Contract — Pod RPC protocol  (Console <-> Launcher Service)
// =============================================================================
// The framed, JSON-based replacement for the old OFB-encrypted BinaryFormatter
// RPC. Runs ON TOP of the same OFB-encrypted stream (NegotiateCryptoStreams is
// unchanged); only the per-message serialization changed.
//
// Wire format, after the OFB/CONF handshake:
//   request : [4-byte little-endian length][UTF-8 JSON RpcRequest]
//   response: [4-byte little-endian length][UTF-8 JSON RpcResponse]
// The InitiateInstallProduct out-of-band file transfer (8-byte length + raw
// bytes) is unchanged and still follows its response frame on the same stream.
//
// Dispatch is by method NAME (RpcRequest.Method) — the old serialized-MethodBase
// + SerializationBinder + MethodInfoProxy machinery is gone. Both ends share this
// one file, so the request/response shape cannot drift.
// =============================================================================

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;

namespace Tesla.Net
{
    /// <summary>One RPC call: a method name plus its arguments as JSON elements.</summary>
    public sealed class RpcRequest
    {
        public string Method { get; set; }
        public List<JsonElement> Args { get; set; }
    }

    /// <summary>One RPC result: the return value as JSON, or an error message.</summary>
    public sealed class RpcResponse
    {
        public JsonElement Result { get; set; }   // JsonValueKind.Null for void / null
        public string Error { get; set; }          // null on success
    }

    /// <summary>Framing + (de)serialization shared by the Console client and the
    /// Launcher Service. Synchronous to match the existing stream usage.</summary>
    public static class PodRpc
    {
        /// <summary>Upper bound on a single JSON frame (the bulk install payload is
        /// streamed out-of-band, not framed), guarding against hostile lengths.</summary>
        public const int MaxFrameBytes = 16 * 1024 * 1024;

        public static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions
        {
            // The Tesla.Net wire types (LaunchData, LaunchPair, ...) expose public
            // FIELDS, which System.Text.Json ignores unless this is set.
            IncludeFields = true,
        };

        // ── Framing ──────────────────────────────────────────────────────────

        public static void WriteFrame(Stream stream, byte[] payload)
        {
            if (payload.Length > MaxFrameBytes)
                throw new IOException($"RPC frame too large ({payload.Length} bytes).");
            var len = BitConverter.GetBytes(payload.Length); // little-endian on x86/x64
            stream.Write(len, 0, 4);
            stream.Write(payload, 0, payload.Length);
            stream.Flush();
        }

        public static byte[] ReadFrame(Stream stream)
        {
            var lenBuf = ReadExact(stream, 4);
            int len = BitConverter.ToInt32(lenBuf, 0);
            if (len < 0 || len > MaxFrameBytes)
                throw new IOException($"RPC frame length out of range ({len}).");
            return ReadExact(stream, len);
        }

        private static byte[] ReadExact(Stream stream, int count)
        {
            var buf = new byte[count];
            int off = 0;
            while (off < count)
            {
                int n = stream.Read(buf, off, count - off);
                if (n == 0) throw new EndOfStreamException("Connection closed mid-frame.");
                off += n;
            }
            return buf;
        }

        // ── Request ──────────────────────────────────────────────────────────

        public static void WriteRequest(Stream stream, string method, object[] args)
        {
            var req = new RpcRequest { Method = method, Args = new List<JsonElement>() };
            if (args != null)
                foreach (var a in args)
                    req.Args.Add(JsonSerializer.SerializeToElement(a, JsonOptions));
            WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(req, JsonOptions));
        }

        public static RpcRequest ReadRequest(Stream stream)
            => JsonSerializer.Deserialize<RpcRequest>(ReadFrame(stream), JsonOptions);

        // ── Response ─────────────────────────────────────────────────────────

        public static void WriteResponse(Stream stream, object result, string error)
        {
            var resp = new RpcResponse
            {
                // Always a valid element (JSON null when there is no result): a
                // default(JsonElement) is ValueKind.Undefined and is not serializable.
                Result = JsonSerializer.SerializeToElement(error == null ? result : null, JsonOptions),
                Error = error,
            };
            WriteFrame(stream, JsonSerializer.SerializeToUtf8Bytes(resp, JsonOptions));
        }

        public static RpcResponse ReadResponse(Stream stream)
            => JsonSerializer.Deserialize<RpcResponse>(ReadFrame(stream), JsonOptions);
    }
}
