using Models;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Text;
using System.Text.Json;

namespace LocalServices.Data
{
    /// <summary>
    /// Database implementation with catastrophic performances.
    /// This database implementation persists data in json and will save all the data in their files for each mutable requests.
    /// </summary>
    public class CatastrophicPerformancesDatabase : IDatabase
    {

        private static readonly string RECIPES_FILENAME = "recipes_data.xml";
        private static readonly string USERS_FILENAME = "users_data.xml";
        private static readonly string ACCOUNTS_FILENAME = "accounts_data.xml";

        private readonly Dictionary<Guid, RecipeData> recipesData;
        private readonly Dictionary<Guid, UserData> usersData;
        private readonly Dictionary<string, AccountData> accountsData;

        private readonly string dbPath;


        public CatastrophicPerformancesDatabase(string folderPath)
        {
            dbPath = folderPath;
            if (!Directory.Exists(folderPath))
                Directory.CreateDirectory(folderPath);

            usersData = Load<Guid, UserData>(USERS_FILENAME);
            recipesData = Load<Guid, RecipeData>(RECIPES_FILENAME);
            accountsData = Load<string, AccountData>(ACCOUNTS_FILENAME);
        }

        public bool IsEmpty()
        {
            return recipesData.Count == 0 && usersData.Count == 0 && accountsData.Count == 0;
        }

        public Account? GetAccount(string email, string passwordHash)
        {
            if (!accountsData.TryGetValue(email, out AccountData? data))
                return null;
            
            if (data.PasswordHash != passwordHash) return null;
            return new Account(usersData[data.UserId].User, data.Email);
        }

        public void InsertAccount(Account account, string passwordHash)
        {
            accountsData[account.Email] = new AccountData(account.User.Id, account.Email, passwordHash);
            Save(ACCOUNTS_FILENAME, accountsData);
            InsertUser(account.User);
        }

        public Recipe? GetRecipe(Guid id)
        {
            if (recipesData.TryGetValue(id, out RecipeData? data))
                return ConvertRecipeDataToRecipe(data);
            return null;
        }

        public RecipeRate GetRecipeRate(Guid user, Guid recipe)
        {
            return usersData[user].Rates[recipe];
        }

        public void InsertInUserList(Guid userId, Guid recipeId, uint persAmount)
        {
            usersData[userId].RecipesList[recipeId] = persAmount;
            Save(USERS_FILENAME, usersData);
        }

        public void RemoveFromUserList(Guid userId, Guid recipeId)
        {
            usersData[userId].RecipesList.Remove(recipeId);
            Save(USERS_FILENAME, usersData);
        }


        public void InsertRecipe(Recipe recipe)
        {
            recipesData[recipe.Info.Id] = new RecipeData(recipe.Info, recipe.Owner.Id, recipe.Ingredients, recipe.Steps);
            Save(RECIPES_FILENAME, recipesData);
        }

        public void InsertUser(User user)
        {
            usersData[user.Id] = new UserData(user, new Dictionary<Guid, RecipeRate>(), new Dictionary<Guid, uint>());
            Save(USERS_FILENAME, usersData);
        }

        public void InsertRate(Guid userId, Guid recipeId, RecipeRate rate)
        {
            usersData[userId].Rates[recipeId] = rate;
            Save(USERS_FILENAME, usersData);
        }

        public void RemoveRecipe(Guid id)
        {
            recipesData.Remove(id);
            Save(RECIPES_FILENAME, recipesData);
        }

        public ImmutableList<Recipe> ListAllRecipes()
        {
            return recipesData.Values.ToImmutableList().ConvertAll(ConvertRecipeDataToRecipe);
        }


        public ImmutableDictionary<Guid, RecipeRate> ListRatesOf(Guid user)
        {
            return usersData[user].Rates.ToImmutableDictionary();
        }

        public ImmutableDictionary<Guid, uint> GetRecipeListOf(Guid user)
        {
            return usersData[user].RecipesList.ToImmutableDictionary();
        }

        private Recipe ConvertRecipeDataToRecipe(RecipeData rd)
        {
            var owner = usersData[rd.OwnerID].User;
            return new Recipe(rd.Info, owner, rd.Ingredients, rd.Steps);
        }

        private Dictionary<K, V> Load<K, V>(string fileName)
        {
            var file = dbPath + "/" + fileName;
            var fileInfo = new FileInfo(file);

            if (!fileInfo.Exists)
                fileInfo.Create();

            if (fileInfo.Length == 0)
                return new Dictionary<K, V>(); //file is empty thus there is nothing to deserialize

            string text = File.ReadAllText(file);

            return JsonSerializer.Deserialize<Dictionary<K, V>>(text);
        }

        private async void Save<K, T>(string fileName, Dictionary<K, T> dict)
        {
            string json = JsonSerializer.Serialize(dict);
            using (var stream = WaitForFile(dbPath + "/" + fileName, FileMode.Open, FileAccess.Write, FileShare.Write))
            {
                var bytes = Encoding.ASCII.GetBytes(json);
                await stream.WriteAsync(bytes, 0, bytes.Length);
            }
        }

        // This is a workaround function to wait for a file to be released before opening it.
        // This was to fix the Save method that used to throw sometimes as the file were oftenly being scanned by the androids' antivirus.
        // Simply wait until the file is released and return it. This function will never return until
        private static FileStream WaitForFile(string fullPath, FileMode mode, FileAccess access, FileShare share)
        {
            for (int attempt = 0 ; attempt < 40; attempt++)
            {
                FileStream? fs = null;
                try
                {
                    fs = new FileStream(fullPath, mode, access, share);
                    return fs;
                }
                catch (FileNotFoundException e)
                {
                    throw e;
                }
                catch (IOException e)
                {
                    Debug.WriteLine(e.Message + " in thread " + Thread.CurrentThread.Name + " " + Thread.CurrentThread.ManagedThreadId);
                    if (fs != null)
                        fs.Dispose();

                    Thread.Sleep(200);
                }
            }
            throw new TimeoutException("Could not access file '" + fullPath + "', maximum attempts reached.");
        }
    }
}
