using System;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Web;

namespace atakanozbancom.Models.classes
{
    public static class TotpHelper
    {
        private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
        private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        public static string GenerateSecret(int byteLength = 20)
        {
            var bytes = new byte[byteLength];
            using (var rng = RandomNumberGenerator.Create())
            {
                rng.GetBytes(bytes);
            }
            return ToBase32(bytes);
        }

        public static bool VerifyCode(string base32Secret, string code, int window = 1)
        {
            if (string.IsNullOrWhiteSpace(base32Secret) || string.IsNullOrWhiteSpace(code))
                return false;

            code = code.Trim().Replace(" ", "");
            if (code.Length != 6)
                return false;

            long timestep;
            try
            {
                timestep = GetCurrentTimeStep();
            }
            catch
            {
                return false;
            }

            for (var i = -window; i <= window; i++)
            {
                var expected = ComputeTotp(base32Secret, timestep + i);
                if (FixedTimeEquals(expected, code))
                    return true;
            }

            return false;
        }

        public static string BuildOtpAuthUri(string issuer, string accountName, string base32Secret)
        {
            var label = HttpUtility.UrlEncode(issuer + ":" + accountName);
            var issuerParam = HttpUtility.UrlEncode(issuer);
            return string.Format(
                CultureInfo.InvariantCulture,
                "otpauth://totp/{0}?secret={1}&issuer={2}&digits=6&period=30",
                label,
                base32Secret,
                issuerParam);
        }

        private static long GetCurrentTimeStep()
        {
            return (long)Math.Floor((DateTime.UtcNow - UnixEpoch).TotalSeconds / 30.0);
        }

        private static string ComputeTotp(string base32Secret, long timestep)
        {
            var key = FromBase32(base32Secret);
            var counter = BitConverter.GetBytes(timestep);
            if (BitConverter.IsLittleEndian)
                Array.Reverse(counter);

            byte[] hash;
            using (var hmac = new HMACSHA1(key))
            {
                hash = hmac.ComputeHash(counter);
            }

            var offset = hash[hash.Length - 1] & 0x0F;
            var binary =
                ((hash[offset] & 0x7F) << 24)
                | ((hash[offset + 1] & 0xFF) << 16)
                | ((hash[offset + 2] & 0xFF) << 8)
                | (hash[offset + 3] & 0xFF);

            var otp = binary % 1000000;
            return otp.ToString("D6", CultureInfo.InvariantCulture);
        }

        private static string ToBase32(byte[] data)
        {
            if (data == null || data.Length == 0)
                return string.Empty;

            var sb = new StringBuilder((data.Length * 8 + 4) / 5);
            int buffer = data[0];
            var next = 1;
            var bitsLeft = 8;

            while (bitsLeft > 0 || next < data.Length)
            {
                if (bitsLeft < 5)
                {
                    if (next < data.Length)
                    {
                        buffer <<= 8;
                        buffer |= data[next++] & 0xFF;
                        bitsLeft += 8;
                    }
                    else
                    {
                        var pad = 5 - bitsLeft;
                        buffer <<= pad;
                        bitsLeft += pad;
                    }
                }

                var index = (buffer >> (bitsLeft - 5)) & 0x1F;
                bitsLeft -= 5;
                sb.Append(Base32Alphabet[index]);
            }

            return sb.ToString();
        }

        private static byte[] FromBase32(string input)
        {
            var cleaned = input.Trim().Replace(" ", "").Replace("=", "").ToUpperInvariant();
            var output = new byte[cleaned.Length * 5 / 8];
            var bitBuffer = 0;
            var bitsLeft = 0;
            var index = 0;

            foreach (var c in cleaned)
            {
                var val = Base32Alphabet.IndexOf(c);
                if (val < 0)
                    throw new FormatException("Invalid Base32 character.");

                bitBuffer = (bitBuffer << 5) | val;
                bitsLeft += 5;

                if (bitsLeft >= 8)
                {
                    output[index++] = (byte)((bitBuffer >> (bitsLeft - 8)) & 0xFF);
                    bitsLeft -= 8;
                }
            }

            if (index != output.Length)
            {
                var trimmed = new byte[index];
                Array.Copy(output, trimmed, index);
                return trimmed;
            }

            return output;
        }

        private static bool FixedTimeEquals(string a, string b)
        {
            if (a == null || b == null || a.Length != b.Length)
                return false;

            var diff = 0;
            for (var i = 0; i < a.Length; i++)
                diff |= a[i] ^ b[i];

            return diff == 0;
        }
    }
}
