using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;

namespace InABox.Core
{
    public static class LicenseUtils
    {
        #region Mac Addresses
        
        public static String[] GetMacAddresses()
        {
            return NetworkInterface
                .GetAllNetworkInterfaces()
                .Where(nic =>
                    nic.OperationalStatus == OperationalStatus.Up &&
                    nic.NetworkInterfaceType != NetworkInterfaceType.Loopback)
                .Select(nic => nic.GetPhysicalAddress().ToString()).ToArray();
        }

        public static bool ValidateMacAddresses(String[] addresses)
        {
            var hardware = GetMacAddresses();
            return hardware.Any(addresses.Contains);
        }
        
        #endregion
        
        #region License Generation
        
        public static LicenseData RenewLicense(LicenseData oldLicense, DateTime renewed, DateTime newExpiry, DateTime renewAvailable, String[] addresses)
        {
            return new LicenseData
            {
                LastRenewal = renewed,
                Expiry = newExpiry,
                CustomerID = oldLicense?.CustomerID ?? Guid.Empty,
                RenewalAvailable = renewAvailable,
                Addresses = addresses,
                IsDynamic = oldLicense?.IsDynamic ?? false
            };
        }
        
        private static readonly byte[] LicenseKey = Convert.FromBase64String("dCyTyQkj1o1rqJJQlT+Jcnkxr+OQnO4KCoF/b+6cx54=");
        
        public static string? EncryptLicenseRequest(LicenseRequest request)
        {
            return Encryption.EncryptV2(Serialization.Serialize(request), LicenseKey);
        }
        
        public static bool TryEncryptLicenseRequest(LicenseRequest request, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error)
        {
            return Encryption.TryEncryptV2(Serialization.Serialize(request), LicenseKey, out result, out error);
        }
        
        /// <summary>
        /// Decrypts <paramref name="request"/>.
        /// </summary>
        /// <param name="request">The request to decrypt.</param>
        /// <returns>
        /// The new license request, or <see langword="null"/> if errors occurred.
        /// </returns>
        public static bool TryDecryptLicenseRequest(string data, [NotNullWhen(true)] out LicenseRequest? result, [NotNullWhen(false)] out string? error)
        {
            if (!Encryption.TryDecryptV2(data, LicenseKey, out var decrypted, out error))
            {
                result = null;
                return false;
            }

            result = Serialization.Deserialize<LicenseRequest>(decrypted);
            if(result == null)
            {
                error = "Request deserialization failed";
                return false;
            }
            return true;
        }
        /// <summary>
        /// Decrypts <paramref name="request"/>, throwing an <see cref="Exception"/> on fail.
        /// </summary>
        /// <param name="request">The data to decrypt.</param>
        /// <returns>
        /// The new license request.
        /// </returns>
        public static LicenseRequest DecryptLicenseRequest(string data)
        {
            if (!TryDecryptLicenseRequest(data, out var result, out var error))
                throw new Exception(error);
            return result;
        }

        /// <summary>
        /// Encrypts the license data.
        /// </summary>
        /// <param name="license">The license to encrypt.</param>
        /// <returns>
        /// The encrypted data.
        /// </returns>
        public static string? EncryptLicense(LicenseData license)
        {
            return Encryption.EncryptV2(Serialization.Serialize(license), LicenseKey);
        }
        
        public static bool TryEncryptLicense(LicenseData license, [NotNullWhen(true)] out string? result, [NotNullWhen(false)] out string? error)
        {
            return Encryption.TryEncryptV2(Serialization.Serialize(license), LicenseKey, out result, out error);
        }

        /// <summary>
        /// Decrypts <paramref name="license"/>.
        /// </summary>
        /// <param name="license">The license to decrypt.</param>
        /// <returns>
        /// The new license data, or <see langword="null"/> if errors occurred.
        /// </returns>
        public static bool TryDecryptLicense(string data, [NotNullWhen(true)] out LicenseData? result, [NotNullWhen(false)] out string? error)
        {
            if (!Encryption.TryDecryptV2(data, LicenseKey, out var decrypted, out error))
            {
                result = null;
                return false;
            }

            result = Serialization.Deserialize<LicenseData>(decrypted);
            if(result == null)
            {
                error = "License deserialization failed";
                return false;
            }
            return true;
        }
        /// <summary>
        /// Decrypts <paramref name="license"/>, throwing an <see cref="Exception"/> on fail.
        /// </summary>
        /// <param name="license">The license to decrypt.</param>
        /// <returns>
        /// The new license data.
        /// </returns>
        public static LicenseData DecryptLicense(string data)
        {
            if (!TryDecryptLicense(data, out var result, out var error))
                throw new Exception(error);
            return result;
        }

        #endregion

        #region Fees & Discounts

        private static readonly Dictionary<String, double> _licensefees = new Dictionary<String, double>();

        private static readonly Dictionary<int, double> _periods = new Dictionary<int, double>();

        private static readonly Dictionary<int, double> _userDiscounts = new Dictionary<int, double>();

        public static double GetLicenseFee(String type)
        {
            return _licensefees.GetValueOrDefault(type, 0);
        }

        public static double GetLicenseFee<T>() where T : LicenseToken
        {
            return GetLicenseFee(typeof(T).EntityName());
        }

        public static IEnumerable<String> LicenseTypes()
        {
            return _licensefees.Keys;
        }

        public static double GetUserDiscount(int users)
        {
            var key = _userDiscounts.Keys
                .Where(x => x <= users)
                .DefaultIfEmpty(-1)
                .Max();
            if (key == -1)
                return 0;
            return _userDiscounts[key];
        }

        public static double GetTimeDiscount(int months)
        {
            var period = _periods.Keys
                .Where(x => x <= months)
                .DefaultIfEmpty(-1)
                .Max();
            if (period == -1)
                return 0;
            return _periods[period];
        }

        public static IEnumerable<int> TimeDiscountLevels()
        {
            return _periods.Keys;
        }

        public static double CalculateLicenseFee(Dictionary<String, int> licenses, int time)
        {
            double licensefee = 0.00F;
            foreach (var license in licenses.Keys)
                if (_licensefees.ContainsKey(license))
                    licensefee += _licensefees[license] * licenses[license];

            var users = licenses.Values.OrderBy(x => x).LastOrDefault();
            var userdiscount = GetUserDiscount(users);

            var timediscount = GetTimeDiscount(time);

            var result = licensefee * ((100.0F - userdiscount) / 100.0F) * ((100.0F - timediscount) / 100.0F);

            return result;
        }

        public static void LoadSummary(LicenseFeeResponse feeResponse)
        {
            _licensefees.Clear();
            _periods.Clear();
            _userDiscounts.Clear();

            foreach (var license in feeResponse.LicenseFees)
                _licensefees[license.Key] = license.Value;
            foreach (var (months, period) in feeResponse.TimeDiscounts)
                _periods[months] = period;
            foreach (var (users, discount) in feeResponse.UserDiscounts)
                _userDiscounts[users] = discount;
        }

        /// <summary>
        /// Gets a map from entities to their associated licenses.
        /// </summary>
        /// <returns>A map of entity types to license types.</returns>
        public static Dictionary<Type, Type> LicenseMap()
        {
            var result = new Dictionary<Type, Type>();

            var entities = CoreUtils.Entities.Where(x => x.IsClass && !x.IsGenericType && x.IsSubclassOf(typeof(Entity)));
            foreach (var entity in entities)
            {
                var lic = entity.GetInterfaces()
                    .FirstOrDefault(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(ILicense<>));
                if (lic != null)
                    result[entity] = lic.GenericTypeArguments.FirstOrDefault();
            }

            return result;
        }

        public static void SaveLicenseSummary(string filename)
        {
            var entitymap = LicenseMap();
            var summary = new List<string> { "\"License Token\",\"Class Name\"" };
            foreach (var mapentry in entitymap.OrderBy(x => x.Value))
                summary.Add(string.Format("\"{0}\",\"{1}\"", mapentry.Value?.GetCaption(), mapentry.Value.EntityName()));
            File.WriteAllLines(filename, summary.ToArray());
        }

        public static void Reset()
        {
            _licensefees.Clear();
            _periods.Clear();
            _userDiscounts.Clear();
        }

        #endregion
    }

    public class LicenseFeeRequest
    {
        public Guid RegistrationID { get; set; }
    }
    
    public class LicenseFeeResponse
    {
        public LicenseFeeResponse()
        {
            LicenseFees = new Dictionary<string, double>();
            TimeDiscounts = new Dictionary<int, double>();
            UserDiscounts = new Dictionary<int, double>();
        }
        
        public Dictionary<string, double> LicenseFees { get; set; }
        public Dictionary<int, double> TimeDiscounts { get; set; }
        public Dictionary<int, double> UserDiscounts { get; set; }
    }
}