//-----------------------------------------------------------------------
// <copyright file="SettingsService.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------

namespace AliasVault.Client.Services;

using System;
using System.Text.Json;
using System.Threading.Tasks;
using AliasClientDb;
using Microsoft.EntityFrameworkCore;

/// <summary>
/// Service class for accessing and mutating general settings stored in database.
/// </summary>
/// <remarks>Note: this service does not use DI but instead is initialized by and can be accessed through the DbService.
/// This is done because the SettingsService requires a DbContext during initialization and the context is not yet
/// available during application boot because of encryption/decryption of remote database file. When accessing the
/// settings through the DbService we can ensure proper data flow.</remarks>
public sealed class SettingsService
{
    private readonly Dictionary<string, string?> _settings = new();
    private DbService? _dbService;
    private bool _initialized;

    /// <summary>
    /// Gets the DefaultEmailDomain setting.
    /// </summary>
    /// <returns>Default email domain as string.</returns>
    public string DefaultEmailDomain => GetSetting("DefaultEmailDomain");

    /// <summary>
    /// Gets a value indicating whether email refresh should be done automatically on the credentials page.
    /// </summary>
    /// <returns>AutoEmailRefresh setting as string.</returns>
    public bool AutoEmailRefresh => GetSetting("AutoEmailRefresh", true);

    /// <summary>
    /// Gets the DefaultIdentityLanguage setting.
    /// </summary>
    /// <returns>Default identity language as two-letter code.</returns>
    public string DefaultIdentityLanguage => GetSetting<string>("DefaultIdentityLanguage", "en")!;

    /// <summary>
    /// Sets the DefaultEmailDomain setting.
    /// </summary>
    /// <param name="value">The new DefaultEmailDomain setting.</param>
    /// <returns>Task.</returns>
    public Task SetDefaultEmailDomain(string value) => SetSettingAsync("DefaultEmailDomain", value);

    /// <summary>
    /// Sets the AutoEmailRefresh setting as a string.
    /// </summary>
    /// <param name="value">The new value.</param>
    /// <returns>Task.</returns>
    public Task SetAutoEmailRefresh(bool value) => SetSettingAsync("AutoEmailRefresh", value);

    /// <summary>
    /// Sets the DefaultIdentityLanguage setting.
    /// </summary>
    /// <param name="value">The new value.</param>
    /// <returns>Task.</returns>
    public Task SetDefaultIdentityLanguage(string value) => SetSettingAsync("DefaultIdentityLanguage", value);

    /// <summary>
    /// Initializes the settings service asynchronously.
    /// </summary>
    /// <param name="dbService">DbService instance.</param>
    /// <returns>Task.</returns>
    public async Task InitializeAsync(DbService dbService)
    {
        if (_initialized)
        {
            return;
        }

        // Store the DbService instance for later use.
        _dbService = dbService;

        var db = await _dbService.GetDbContextAsync();
        var settings = await db.Settings.ToListAsync();
        foreach (var setting in settings)
        {
            _settings[setting.Key] = setting.Value;
        }

        _initialized = true;
    }

    /// <summary>
    /// Casts a setting value from the database string type to the specified requested type.
    /// </summary>
    /// <param name="value">Value (string) to cast.</param>
    /// <typeparam name="T">Type to cast it to.</typeparam>
    /// <returns>The value cast to the requested type.</returns>
    private static T? CastSetting<T>(string value)
    {
        if (string.IsNullOrEmpty(value))
        {
            if (default(T) is null)
            {
                return default;
            }

            throw new InvalidOperationException($"Setting value is null or empty for non-nullable type {typeof(T)}");
        }

        if (typeof(T) == typeof(bool))
        {
            return (T)(object)(bool.TryParse(value, out bool result) && result);
        }

        if (typeof(T) == typeof(int))
        {
            return (T)(object)int.Parse(value);
        }

        if (typeof(T) == typeof(double))
        {
            return (T)(object)double.Parse(value);
        }

        if (typeof(T) == typeof(string))
        {
            return (T)(object)value;
        }

        // For complex types, attempt JSON deserialization
        try
        {
            var result = JsonSerializer.Deserialize<T>(value);
            if (result is null && default(T) is not null)
            {
                throw new InvalidOperationException($"Deserialization resulted in null for non-nullable type {typeof(T)}");
            }

            return result;
        }
        catch (JsonException ex)
        {
            throw new InvalidOperationException($"Failed to deserialize value to type {typeof(T)}", ex);
        }
    }

    /// <summary>
    /// Converts a value of any type to a string.
    /// </summary>
    /// <param name="value">The value to convert.</param>
    /// <typeparam name="T">The type of the existing value.</typeparam>
    /// <returns>Value converted to string.</returns>
    private static string ConvertToString<T>(T value)
    {
        if (value is bool || value is int || value is double || value is string)
        {
            return value.ToString() ?? string.Empty;
        }

        // For complex types, use JSON serialization
        return JsonSerializer.Serialize(value);
    }

    /// <summary>
    /// Get setting value from database.
    /// </summary>
    /// <param name="key">Key of setting to retrieve.</param>
    /// <returns>Setting as string value.</returns>
    private string GetSetting(string key)
    {
        var setting = _settings.GetValueOrDefault(key);
        return setting ?? string.Empty;
    }

    /// <summary>
    /// Gets a setting and casts it to the specified type.
    /// </summary>
    /// <typeparam name="T">The type to cast the setting to.</typeparam>
    /// <param name="key">The key of the setting.</param>
    /// <param name="defaultValue">The default value to use if no setting is set in database.</param>
    /// <returns>The setting value cast to type T.</returns>
    private T? GetSetting<T>(string key, T? defaultValue = default)
    {
        string value = GetSetting(key);

        if (string.IsNullOrEmpty(value))
        {
            // If no value is available in database but default value is set, return default value.
            if (defaultValue is not null)
            {
                return defaultValue;
            }

            // No value in database and no default value set, throw exception.
            throw new InvalidOperationException($"Setting {key} is not set and no default value is provided");
        }

        try
        {
            return CastSetting<T>(value);
        }
        catch (InvalidOperationException ex)
        {
            throw new InvalidOperationException($"Failed to cast setting {key} to type {typeof(T)}", ex);
        }
    }

    /// <summary>
    /// Sets a setting asynchronously, converting the value to a string so its compatible with the database field.
    /// </summary>
    /// <typeparam name="T">The type of the value being set.</typeparam>
    /// <param name="key">The key of the setting.</param>
    /// <param name="value">The value to set.</param>
    /// <returns>Task.</returns>
    private Task SetSettingAsync<T>(string key, T value)
    {
        string stringValue = ConvertToString(value);
        return SetSettingAsync(key, stringValue);
    }

    /// <summary>
    /// Set setting value in database.
    /// </summary>
    /// <param name="key">Key of setting to set.</param>
    /// <param name="value">Value of setting to set.</param>
    /// <returns>Task.</returns>
    private async Task SetSettingAsync(string key, string value)
    {
        var db = await _dbService!.GetDbContextAsync();
        var setting = await db.Settings.FindAsync(key);
        if (setting == null)
        {
            setting = new Setting
            {
                Key = key,
                Value = value,
                CreatedAt = DateTime.UtcNow,
                UpdatedAt = DateTime.UtcNow,
            };
            db.Settings.Add(setting);
        }
        else
        {
            setting.Value = value;
            setting.UpdatedAt = DateTime.UtcNow;
            db.Settings.Update(setting);
        }

        // Also update the setting in the local dictionary so the new value
        // is returned by subsequent local reads.
        _settings[key] = value;

        await _dbService.SaveDatabaseAsync();
    }
}
