using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers.Cache;
using Timeline.Models;
using Timeline.Services.Exceptions;
namespace Timeline.Services
{
    /// 
    /// Provider for default user avatar.
    /// 
    /// 
    /// Mainly for unit tests.
    /// 
    public interface IDefaultUserAvatarProvider
    {
        /// 
        /// Get the digest of default avatar.
        /// 
        /// The digest.
        Task GetDefaultAvatarDigest();
        /// 
        /// Get the default avatar.
        /// 
        /// The avatar.
        Task GetDefaultAvatar();
    }
    public interface IUserAvatarService
    {
        /// 
        /// Get avatar digest of a user.
        /// 
        /// User id.
        /// The avatar digest.
        /// Thrown when user does not exist.
        Task GetAvatarDigest(long userId);
        /// 
        /// Get avatar of a user. If the user has no avatar set, a default one is returned.
        /// 
        /// User id.
        /// The avatar.
        /// Thrown when user does not exist.
        Task GetAvatar(long userId);
        /// 
        /// Set avatar for a user.
        /// 
        /// User id.
        /// The new avatar data.
        /// The digest of the avatar.
        /// Thrown if  is null.
        /// Thrown when user does not exist.
        /// Thrown if avatar is of bad format.
        Task SetAvatar(long userId, ByteData avatar);
        /// 
        /// Remove avatar of a user.
        /// 
        /// User id.
        /// Thrown when user does not exist.
        Task DeleteAvatar(long userId);
    }
    // TODO! : Make this configurable.
    public class DefaultUserAvatarProvider : IDefaultUserAvatarProvider
    {
        private readonly IETagGenerator _eTagGenerator;
        private readonly string _avatarPath;
        private CacheableDataDigest? _cacheDigest;
        private ByteData? _cacheData;
        public DefaultUserAvatarProvider(IWebHostEnvironment environment, IETagGenerator eTagGenerator)
        {
            _avatarPath = Path.Combine(environment.ContentRootPath, "default-avatar.png");
            _eTagGenerator = eTagGenerator;
        }
        private async Task CheckAndInit()
        {
            var path = _avatarPath;
            if (_cacheData == null || File.GetLastWriteTime(path) > _cacheDigest!.LastModified)
            {
                var data = await File.ReadAllBytesAsync(path);
                _cacheDigest = new CacheableDataDigest(await _eTagGenerator.Generate(data), File.GetLastWriteTime(path));
                Image.Identify(data, out var format);
                _cacheData = new ByteData(data, format.DefaultMimeType);
            }
        }
        public async Task GetDefaultAvatarDigest()
        {
            await CheckAndInit();
            return _cacheDigest!;
        }
        public async Task GetDefaultAvatar()
        {
            await CheckAndInit();
            return _cacheData!;
        }
    }
    public class UserAvatarService : IUserAvatarService
    {
        private readonly ILogger _logger;
        private readonly DatabaseContext _database;
        private readonly IBasicUserService _basicUserService;
        private readonly IDefaultUserAvatarProvider _defaultUserAvatarProvider;
        private readonly IImageValidator _imageValidator;
        private readonly IDataManager _dataManager;
        private readonly IClock _clock;
        public UserAvatarService(
            ILogger logger,
            DatabaseContext database,
            IBasicUserService basicUserService,
            IDefaultUserAvatarProvider defaultUserAvatarProvider,
            IImageValidator imageValidator,
            IDataManager dataManager,
            IClock clock)
        {
            _logger = logger;
            _database = database;
            _basicUserService = basicUserService;
            _defaultUserAvatarProvider = defaultUserAvatarProvider;
            _imageValidator = imageValidator;
            _dataManager = dataManager;
            _clock = clock;
        }
        public async Task GetAvatarDigest(long userId)
        {
            var usernameChangeTime = await _basicUserService.GetUsernameLastModifiedTime(userId);
            var entity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.DataTag, a.LastModified }).SingleOrDefaultAsync();
            if (entity is null)
            {
                var defaultAvatarDigest = await _defaultUserAvatarProvider.GetDefaultAvatarDigest();
                return new CacheableDataDigest(defaultAvatarDigest.ETag, new DateTime[] { usernameChangeTime, defaultAvatarDigest.LastModified }.Max());
            }
            else if (entity.DataTag is null)
            {
                var defaultAvatarDigest = await _defaultUserAvatarProvider.GetDefaultAvatarDigest();
                return new CacheableDataDigest(defaultAvatarDigest.ETag, new DateTime[] { usernameChangeTime, defaultAvatarDigest.LastModified, entity.LastModified }.Max());
            }
            else
            {
                return new CacheableDataDigest(entity.DataTag, new DateTime[] { usernameChangeTime, entity.LastModified }.Max());
            }
        }
        public async Task GetAvatar(long userId)
        {
            await _basicUserService.ThrowIfUserNotExist(userId);
            var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
            if (entity is null || entity.DataTag is null)
            {
                return await _defaultUserAvatarProvider.GetDefaultAvatar();
            }
            var data = await _dataManager.GetEntryAndCheck(entity.DataTag, $"This is required by avatar of {userId}.");
            if (entity.Type is null)
            {
                Image.Identify(data, out var format);
                entity.Type = format.DefaultMimeType;
                await _database.SaveChangesAsync();
            }
            return new ByteData(data, entity.Type);
        }
        public async Task SetAvatar(long userId, ByteData avatar)
        {
            if (avatar is null)
                throw new ArgumentNullException(nameof(avatar));
            await _imageValidator.Validate(avatar.Data, avatar.ContentType, true);
            await _basicUserService.ThrowIfUserNotExist(userId);
            var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
            await using var transaction = await _database.Database.BeginTransactionAsync();
            var tag = await _dataManager.RetainEntry(avatar.Data);
            var now = _clock.GetCurrentTime();
            if (entity is null)
            {
                var newEntity = new UserAvatarEntity
                {
                    DataTag = tag,
                    Type = avatar.ContentType,
                    LastModified = now,
                    UserId = userId
                };
                _database.Add(newEntity);
            }
            else
            {
                if (entity.DataTag is not null)
                    await _dataManager.FreeEntry(entity.DataTag);
                entity.DataTag = tag;
                entity.Type = avatar.ContentType;
                entity.LastModified = now;
            }
            await _database.SaveChangesAsync();
            await transaction.CommitAsync();
            return new CacheableDataDigest(tag, now);
        }
        public async Task DeleteAvatar(long userId)
        {
            await _basicUserService.ThrowIfUserNotExist(userId);
            var entity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync();
            if (entity is null || entity.DataTag is null)
                return;
            await using var transaction = await _database.Database.BeginTransactionAsync();
            await _dataManager.FreeEntry(entity.DataTag);
            entity.DataTag = null;
            entity.Type = null;
            entity.LastModified = _clock.GetCurrentTime();
            await _database.SaveChangesAsync();
            await transaction.CommitAsync();
        }
    }
    public static class UserAvatarServiceCollectionExtensions
    {
        public static void AddUserAvatarService(this IServiceCollection services)
        {
            services.AddScoped();
            services.AddScoped();
        }
    }
}