#region License

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#endregion

using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text.Json;
using Gremlin.Net.Driver.Messages;

namespace Gremlin.Net.Driver
{
    /// <summary>
    ///     Mutable HTTP request context passed to request interceptors.
    /// </summary>
    public class HttpRequestContext
    {
        /// <summary>
        ///     Gets or sets the HTTP method (e.g. "POST").
        /// </summary>
        public string Method { get; set; }

        /// <summary>
        ///     Gets or sets the request URI.
        /// </summary>
        public Uri Uri { get; set; }

        /// <summary>
        ///     Gets the HTTP headers. Interceptors may add, modify, or remove entries.
        /// </summary>
        public Dictionary<string, string> Headers { get; }

        /// <summary>
        ///     Gets or sets the request body. Initially a <see cref="RequestMessage"/>, becomes
        ///     <c>byte[]</c> after <see cref="SerializeBody"/> is called. Interceptors may also
        ///     set this to an <see cref="System.Net.Http.HttpContent"/> instance for full control
        ///     over the wire format.
        /// </summary>
        public object Body { get; set; }

        /// <summary>
        ///     Initializes a new instance of the <see cref="HttpRequestContext" /> class.
        /// </summary>
        /// <param name="method">The HTTP method.</param>
        /// <param name="uri">The request URI.</param>
        /// <param name="headers">The HTTP headers.</param>
        /// <param name="body">The request body. Typically <c>RequestMessage</c> (pre-serialization)
        ///     or <c>byte[]</c> (post-serialization).</param>
        public HttpRequestContext(string method, Uri uri, Dictionary<string, string> headers, object body)
        {
            Method = method ?? throw new ArgumentNullException(nameof(method));
            Uri = uri ?? throw new ArgumentNullException(nameof(uri));
            Headers = headers ?? throw new ArgumentNullException(nameof(headers));
            Body = body;
        }

        /// <summary>
        ///     Serializes the body to JSON if it is still a <see cref="RequestMessage"/>.
        ///     Sets the body to the resulting <c>byte[]</c>, and sets the <c>Content-Type</c>
        ///     and <c>Content-Length</c> headers. This method is idempotent: if the body is
        ///     already <c>byte[]</c>, it returns those bytes without re-serializing.
        /// </summary>
        /// <returns>The serialized body bytes.</returns>
        /// <exception cref="InvalidOperationException">
        ///     Thrown if the body is neither <see cref="RequestMessage"/> nor <c>byte[]</c>.
        /// </exception>
        public byte[] SerializeBody()
        {
            if (Body is byte[] existing)
            {
                return existing;
            }

            if (Body is RequestMessage message)
            {
                var payload = new Dictionary<string, object>
                {
                    [Tokens.ArgsGremlin] = message.Gremlin
                };
                foreach (var field in message.Fields)
                {
                    payload[field.Key] = field.Value;
                }

                var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(payload);
                Body = jsonBytes;
                Headers["Content-Type"] = "application/json";
                Headers["Content-Length"] = jsonBytes.Length.ToString();
                return jsonBytes;
            }

            throw new InvalidOperationException(
                "Cannot serialize body of type " + (Body?.GetType().Name ?? "null") +
                ". Body must be RequestMessage or byte[].");
        }

        /// <summary>
        ///     Returns the lowercase hex-encoded SHA-256 digest of the body.
        ///     Throws <see cref="InvalidOperationException"/> if <see cref="Body"/> is not <c>byte[]</c>,
        ///     which indicates that serialization has not yet occurred.
        /// </summary>
        public string GetPayloadHash()
        {
            if (Body is not byte[] bytes)
            {
                throw new InvalidOperationException(
                    "Cannot compute payload hash before serialization. " +
                    "Body must be byte[] but is " +
                    (Body?.GetType().Name ?? "null") + ".");
            }
            using var sha256 = SHA256.Create();
            var hash = sha256.ComputeHash(bytes);
            return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
        }
    }
}
