using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SharedLibraryCore.Services
{
    public class MetaService
    {
        private static List<Func<int, int, int, DateTime?, Task<List<ProfileMeta>>>> _metaActions = new List<Func<int, int, int, DateTime?, Task<List<ProfileMeta>>>>();

        /// <summary>
        /// adds or updates meta key and value to the database
        /// </summary>
        /// <param name="metaKey">key of meta data</param>
        /// <param name="metaValue">value of the meta data</param>
        /// <param name="client">client to save the meta for</param>
        /// <returns></returns>
        public async Task AddPersistentMeta(string metaKey, string metaValue, EFClient client)
        {
            // this seems to happen if the client disconnects before they've had time to authenticate and be added
            if (client.ClientId < 1)
            {
                return;
            }

            using (var ctx = new DatabaseContext())
            {
                var existingMeta = await ctx.EFMeta
                    .Where(_meta => _meta.Key == metaKey)
                    .Where(_meta => _meta.ClientId == client.ClientId)
                    .FirstOrDefaultAsync();

                if (existingMeta != null)
                {
                    existingMeta.Value = metaValue;
                    existingMeta.Updated = DateTime.UtcNow;
                }

                else
                {
                    ctx.EFMeta.Add(new EFMeta()
                    {
                        ClientId = client.ClientId,
                        Created = DateTime.UtcNow,
                        Key = metaKey,
                        Value = metaValue
                    });
                }

                await ctx.SaveChangesAsync();
            }
        }

        internal static void Clear()
        {
            _metaActions.Clear();
        }

        /// <summary>
        /// retrieves meta data for given client and key
        /// </summary>
        /// <param name="metaKey">key to retrieve value for</param>
        /// <param name="client">client to retrieve meta for</param>
        /// <returns></returns>
        public async Task<EFMeta> GetPersistentMeta(string metaKey, EFClient client)
        {
            using (var ctx = new DatabaseContext(disableTracking: true))
            {
                return await ctx.EFMeta
                    .Where(_meta => _meta.Key == metaKey)
                    .Where(_meta => _meta.ClientId == client.ClientId)
                    .FirstOrDefaultAsync();
            }
        }

        /// <summary>
        /// aads a meta task to the runtime meta list
        /// </summary>
        /// <param name="metaAction"></param>
        public static void AddRuntimeMeta(Func<int, int, int, DateTime?, Task<List<ProfileMeta>>> metaAction)
        {
            _metaActions.Add(metaAction);
        }

        /// <summary>
        /// retrieves all the runtime meta information for given client idea
        /// </summary>
        /// <param name="clientId">id of the client</param>
        /// <param name="count">number of meta items to retrieve</param>
        /// <param name="offset">offset from the first item</param>
        /// <returns></returns>
        public static async Task<List<ProfileMeta>> GetRuntimeMeta(int clientId, int offset = 0, int count = int.MaxValue, DateTime? startAt = null)
        {
            var meta = new List<ProfileMeta>();

            foreach (var action in _metaActions)
            {
                var metaItems = await action(clientId, offset, count, startAt);
                meta.AddRange(metaItems);
            }

            if (count == 1)
            {
                var table = new List<List<ProfileMeta>>();
                var metaWithColumn = meta
                    .Where(_meta => _meta.Column != null);

                var columnGrouping = metaWithColumn
                    .GroupBy(_meta => _meta.Column);

                var metaToSort = meta.Except(metaWithColumn).ToList();

                foreach (var metaItem in columnGrouping)
                {
                    table.Add(new List<ProfileMeta>(metaItem));
                }

                while (metaToSort.Count > 0)
                {
                    var sortingMeta = metaToSort.First();

                    int indexOfSmallestColumn()
                    {
                        int index = 0;
                        int smallestColumnSize = int.MaxValue;
                        for (int i = 0; i < table.Count; i++)
                        {
                            if (table[i].Count < smallestColumnSize)
                            {
                                smallestColumnSize = table[i].Count;
                                index = i;
                            }
                        }
                        return index;
                    }

                    int columnIndex = indexOfSmallestColumn();

                    sortingMeta.Column = columnIndex;
                    sortingMeta.Order = columnGrouping
                        .First(_group => _group.Key == columnIndex)
                        .Count();

                    table[columnIndex].Add(sortingMeta);

                    metaToSort.Remove(sortingMeta);
                }

                return meta;
            }

            return meta.OrderByDescending(_meta => _meta.When)
                .Take(count)
                .ToList();
        }
    }
}