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

namespace InABox.Core
{
    public static class LicenseUtils
    {
        #region License Generation

        /// <summary>
        /// Generate a new, out-of-the-box license.
        /// </summary>
        /// <returns>The new license, valid for 1 month.</returns>
        public static LicenseData GenerateNewLicense()
        {
            return new LicenseData
            {
                LastRenewal = DateTime.Now,
                Expiry = DateTime.Now.AddMonths(1),
                CustomerID = Guid.Empty,
                RenewalAvailable = DateTime.Now.AddMonths(1).AddDays(-7)
            };
        }

        public static LicenseData RenewLicense(LicenseData oldLicense, DateTime renewed, DateTime newExpiry, DateTime renewAvailable)
        {
            return new LicenseData
            {
                LastRenewal = renewed,
                Expiry = newExpiry,
                CustomerID = oldLicense.CustomerID,
                RenewalAvailable = renewAvailable
            };
        }

        private static readonly byte[] LicenseKey = Convert.FromBase64String("dCyTyQkj1o1rqJJQlT+Jcnkxr+OQnO4KCoF/b+6cx54=");

        /// <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<Type, double> _licensefees = new Dictionary<Type, 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(Type type)
        {
            return _licensefees.GetValueOrDefault(type, 0);
        }

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

        public static IEnumerable<Type> 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<Type, 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(LicenseSummary summary)
        {
            _licensefees.Clear();
            _periods.Clear();
            _userDiscounts.Clear();

            foreach (var license in summary.LicenseFees)
                _licensefees[CoreUtils.GetEntity(license.Key)] = license.Value;
            foreach (var (months, period) in summary.TimeDiscounts)
                _periods[months] = period;
            foreach (var (users, discount) in summary.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 licenses = CoreUtils.TypeList(
                AppDomain.CurrentDomain.GetAssemblies(),
                myType =>
                    myType.IsClass
                    && !myType.IsAbstract
                    && !myType.IsGenericType
                    && myType.IsSubclassOf(typeof(LicenseToken))
            ).ToArray();

            var entities = CoreUtils.TypeList(
                AppDomain.CurrentDomain.GetAssemblies(),
                myType =>
                    myType.IsClass
                    && !myType.IsAbstract
                    && !myType.IsGenericType
                    && myType.IsSubclassOf(typeof(Entity))
            ).ToArray();

            foreach (var entity in entities)
            {
                var lic = entity.GetInterfaces()
                    .FirstOrDefault(x => x.IsConstructedGenericType && x.GetGenericTypeDefinition() == typeof(ILicense<>));
                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 LicenseSummary
    {
        public LicenseSummary()
        {
            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; }
    }
}