using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace CarManagerV3.Util
{
    internal class CUID
    {
        public const int DefaultLength = 6;

        private static int _counter = RandomNumberGenerator.GetInt32(int.MaxValue);

        /// <summary>
        /// Generate a random CUID (Collision-resistant Unique Identifier) of a specified length. 
        /// The CUID is designed to be unique across different machines and time, making it suitable for use as an identifier for cars in the application. 
        /// The length must be between 4 and 32 characters to ensure a good balance between uniqueness and readability.
        /// The generated CUID consists of a combination of alphanumeric characters to ensure uniqueness and readability.
        /// </summary>
        /// <param name="length">The desired length of the generated CUID</param>
        /// <param name="prefixWithC">Whether to prefix the CUID with 'c' for better readability and to avoid starting with a digit.</param>"
        /// <returns></returns>
        public static string NewCUID(int length = DefaultLength, bool prefixWithC = true)
        {
            // CUIDv2 specs allow between 4 and 32 chars.
            if(length < 4 || length > 32) throw new ArgumentOutOfRangeException("length");

            // We will produce enough encoded chars to satisfy 'length' after prefixing and truncation.
            // Base64 encodes 3 bytes -> 4 chars. So bytesNeeded ≈ ceil(charsNeeded * 3/4).
            int charsNeeded = prefixWithC ? (length - 1) : length;
            int bytesNeeded = (int)Math.Ceiling(charsNeeded * 3.0 / 4.0);

            Span<byte> material = stackalloc byte[32];
            FillMaterial(material);

            byte[] outputBytes = ExpandWithSha256(material, bytesNeeded);

            string encoded = Base64UrlEncode(outputBytes);

            if (encoded.Length < charsNeeded)
            {
                // Extremely unlikely unless length is huge; ensure we have enough by expanding more.
                // (Kept as a guard; for typical lengths like 24-64, you're fine.)
                outputBytes = ExpandWithSha256(material, bytesNeeded + 32);
                encoded = Base64UrlEncode(outputBytes);
            }

            string body = encoded.Substring(0, charsNeeded);
            return prefixWithC ? ("c" + body) : body;

        }


        private static void FillMaterial(Span<byte> dst32)
        {
            // Compose a payload with:
            // - 16 bytes random
            // - 8 bytes timestamp (UTC ticks)
            // - 4 bytes counter
            // - 4 bytes process/thread noise
            Span<byte> payload = stackalloc byte[16 + 8 + 4 + 4];

            RandomNumberGenerator.Fill(payload.Slice(0, 16));

            long ticks = DateTime.UtcNow.Ticks;
            BinaryPrimitives.WriteInt64LittleEndian(payload.Slice(16, 8), ticks);

            int c = Interlocked.Increment(ref _counter);
            BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(24, 4), c);

            // Some extra variability (not relied on for security)
            int noise = Environment.ProcessId ^ Thread.CurrentThread.ManagedThreadId ^ (int)Stopwatch.GetTimestamp();
            BinaryPrimitives.WriteInt32LittleEndian(payload.Slice(28, 4), noise);

            // Hash to produce 32 bytes of uniformly distributed output
            SHA256.HashData(payload, dst32);
        }

        private static byte[] ExpandWithSha256(ReadOnlySpan<byte> seed32, int bytesNeeded)
        {
            if (bytesNeeded <= 0) return Array.Empty<byte>();

            // If <= 32 bytes, we can just take from seed32 by hashing once more for separation.
            // We'll use SHA256(seed || blockIndex) to generate blocks.
            int blocks = (int)Math.Ceiling(bytesNeeded / 32.0);
            byte[] result = new byte[blocks * 32];

            Span<byte> input = stackalloc byte[32 + 4];
            seed32.CopyTo(input.Slice(0, 32));

            for (int i = 0; i < blocks; i++)
            {
                BinaryPrimitives.WriteInt32LittleEndian(input.Slice(32, 4), i);
                Span<byte> block = result.AsSpan(i * 32, 32);
                SHA256.HashData(input, block);
            }

            if (result.Length == bytesNeeded) return result;

            byte[] trimmed = new byte[bytesNeeded];
            Buffer.BlockCopy(result, 0, trimmed, 0, bytesNeeded);
            return trimmed;
        }

        private static string Base64UrlEncode(byte[] bytes)
        {
            // Standard base64url without padding per RFC 4648 §5
            string b64 = Convert.ToBase64String(bytes);
            return b64.Replace('+', '-').Replace('/', '_').TrimEnd('=');
        }

    }
}
