using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
using Timeline.Helpers.Cache;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
namespace Timeline.Services
{
    public class TimelinePostCreateRequestData
    {
        public TimelinePostCreateRequestData(string contentType, byte[] data)
        {
            ContentType = contentType;
            Data = data;
        }
        public string ContentType { get; set; }
#pragma warning disable CA1819 // Properties should not return arrays
        public byte[] Data { get; set; }
#pragma warning restore CA1819 // Properties should not return arrays
    }
    public class TimelinePostCreateRequest
    {
        public string? Color { get; set; }
        /// If not set, current time is used.
        public DateTime? Time { get; set; }
#pragma warning disable CA2227
        public List DataList { get; set; } = new List();
#pragma warning restore CA2227
    }
    public class TimelinePostPatchRequest
    {
        public string? Color { get; set; }
        public DateTime? Time { get; set; }
    }
    public interface ITimelinePostService
    {
        /// 
        /// Get all the posts in the timeline.
        /// 
        /// The id of the timeline.
        /// The time that posts have been modified since.
        /// Whether include deleted posts.
        /// A list of all posts.
        /// Thrown when timeline does not exist.
        Task> GetPosts(long timelineId, DateTime? modifiedSince = null, bool includeDeleted = false);
        /// 
        /// Get a post of a timeline.
        /// 
        /// The id of the timeline of the post.
        /// The id of the post.
        /// If true, return the entity even if it is deleted.
        /// The post.
        /// Thrown when timeline does not exist.
        /// Thrown when post of  does not exist or has been deleted.
        Task GetPost(long timelineId, long postId, bool includeDeleted = false);
        /// 
        /// Get the data digest of a post.
        /// 
        /// The timeline id.
        /// The post id.
        /// The index of the data.
        /// The data digest.
        /// Thrown when timeline does not exist.
        /// Thrown when post of  does not exist or has been deleted.
        /// Thrown when data of that index does not exist.
        Task GetPostDataDigest(long timelineId, long postId, long dataIndex);
        /// 
        /// Get the data of a post.
        /// 
        /// The timeline id.
        /// The post id.
        /// The index of the data.
        /// The data.
        /// Thrown when timeline does not exist.
        /// Thrown when post of  does not exist or has been deleted.
        /// Thrown when data of that index does not exist.
        Task GetPostData(long timelineId, long postId, long dataIndex);
        /// 
        /// Create a new post in timeline.
        /// 
        /// The id of the timeline to create post against.
        /// The author's user id.
        /// Info about the post.
        /// The entity of the created post.
        /// Thrown when  is null.
        /// Thrown when  is of invalid format.
        /// Thrown when timeline does not exist.
        /// Thrown if user of  does not exist.
        /// Thrown if data is not a image. Validated by .
        Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request);
        /// 
        /// Modify a post. Change its properties or replace its content.
        /// 
        /// The timeline id.
        /// The post id.
        /// The request.
        /// The entity of the patched post.
        /// Thrown when  is null.
        /// Thrown when  is of invalid format.
        /// Thrown when timeline does not exist.
        /// Thrown when post does not exist.
        Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request);
        /// 
        /// Delete a post.
        /// 
        /// The id of the timeline to delete post against.
        /// The id of the post to delete.
        /// Thrown when timeline does not exist.
        /// Thrown when the post with given id does not exist or is deleted already.
        /// 
        /// First use  to check the permission.
        /// 
        Task DeletePost(long timelineId, long postId);
        /// 
        /// Delete all posts of the given user. Used when delete a user.
        /// 
        /// The id of the user.
        Task DeleteAllPostsOfUser(long userId);
        /// 
        /// Verify whether a user has the permission to modify a post.
        /// 
        /// The id of the timeline.
        /// The id of the post.
        /// The id of the user to check on.
        /// True if you want it to throw . Default false.
        /// True if can modify, false if can't modify.
        /// Thrown when timeline does not exist.
        /// Thrown when the post with given id does not exist or is deleted already and  is true.
        /// 
        /// Unless  is true, this method should return true if the post does not exist.
        /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless  is true.
        /// This method does not check whether the user is administrator.
        /// It only checks whether he is the author of the post or the owner of the timeline.
        /// Return false when user with modifier id does not exist.
        /// 
        Task HasPostModifyPermission(long timelineId, long postId, long modifierId, bool throwOnPostNotExist = false);
    }
    public class TimelinePostService : ITimelinePostService
    {
        private readonly ILogger _logger;
        private readonly DatabaseContext _database;
        private readonly IBasicTimelineService _basicTimelineService;
        private readonly IBasicUserService _basicUserService;
        private readonly IDataManager _dataManager;
        private readonly IImageValidator _imageValidator;
        private readonly IClock _clock;
        private readonly ColorValidator _colorValidator = new ColorValidator();
        public TimelinePostService(ILogger logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IBasicUserService basicUserService, IDataManager dataManager, IImageValidator imageValidator, IClock clock)
        {
            _logger = logger;
            _database = database;
            _basicTimelineService = basicTimelineService;
            _basicUserService = basicUserService;
            _dataManager = dataManager;
            _imageValidator = imageValidator;
            _clock = clock;
        }
        private async Task CheckTimelineExistence(long timelineId)
        {
            if (!await _basicTimelineService.CheckExistence(timelineId))
                throw new TimelineNotExistException(timelineId);
        }
        private async Task CheckUserExistence(long userId)
        {
            if (!await _basicUserService.CheckUserExistence(userId))
                throw new UserNotExistException(userId);
        }
        public async Task> GetPosts(long timelineId, DateTime? modifiedSince = null, bool includeDeleted = false)
        {
            await CheckTimelineExistence(timelineId);
            modifiedSince = modifiedSince?.MyToUtc();
            IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId);
            if (!includeDeleted)
            {
                query = query.Where(p => !p.Deleted);
            }
            if (modifiedSince.HasValue)
            {
                query = query.Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince));
            }
            query = query.OrderBy(p => p.Time);
            return await query.ToListAsync();
        }
        public async Task GetPost(long timelineId, long postId, bool includeDeleted = false)
        {
            await CheckTimelineExistence(timelineId);
            var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
            if (post is null)
            {
                throw new TimelinePostNotExistException(timelineId, postId, false);
            }
            if (!includeDeleted && post.Deleted)
            {
                throw new TimelinePostNotExistException(timelineId, postId, true);
            }
            return post;
        }
        public async Task GetPostDataDigest(long timelineId, long postId, long dataIndex)
        {
            await CheckTimelineExistence(timelineId);
            var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).Select(p => new { p.Id, p.Deleted }).SingleOrDefaultAsync();
            if (postEntity is null)
                throw new TimelinePostNotExistException(timelineId, postId, false);
            if (postEntity.Deleted)
                throw new TimelinePostNotExistException(timelineId, postId, true);
            var dataEntity = await _database.TimelinePostData.Where(d => d.PostId == postEntity.Id && d.Index == dataIndex).SingleOrDefaultAsync();
            if (dataEntity is null)
                throw new TimelinePostDataNotExistException(timelineId, postId, dataIndex);
            return new CacheableDataDigest(dataEntity.DataTag, dataEntity.LastUpdated);
        }
        public async Task GetPostData(long timelineId, long postId, long dataIndex)
        {
            await CheckTimelineExistence(timelineId);
            var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).Select(p => new { p.Id, p.Deleted }).SingleOrDefaultAsync();
            if (postEntity is null)
                throw new TimelinePostNotExistException(timelineId, postId, false);
            if (postEntity.Deleted)
                throw new TimelinePostNotExistException(timelineId, postId, true);
            var dataEntity = await _database.TimelinePostData.Where(d => d.PostId == postEntity.Id && d.Index == dataIndex).SingleOrDefaultAsync();
            if (dataEntity is null)
                throw new TimelinePostDataNotExistException(timelineId, postId, dataIndex);
            var data = await _dataManager.GetEntryAndCheck(dataEntity.DataTag, $"Timeline {timelineId}, post {postId}, data {dataIndex} requires this data.");
            return new ByteData(data, dataEntity.Kind);
        }
        public async Task CreatePost(long timelineId, long authorId, TimelinePostCreateRequest request)
        {
            if (request is null)
                throw new ArgumentNullException(nameof(request));
            {
                if (!_colorValidator.Validate(request.Color, out var message))
                    throw new ArgumentException("Color is not valid.", nameof(request));
            }
            if (request.DataList is null)
                throw new ArgumentException("Data list can't be null.", nameof(request));
            if (request.DataList.Count == 0)
                throw new ArgumentException("Data list can't be empty.", nameof(request));
            if (request.DataList.Count > 100)
                throw new ArgumentException("Data list count can't be bigger than 100.", nameof(request));
            for (int index = 0; index < request.DataList.Count; index++)
            {
                var data = request.DataList[index];
                switch (data.ContentType)
                {
                    case MimeTypes.ImageGif:
                    case MimeTypes.ImageJpeg:
                    case MimeTypes.ImagePng:
                    case MimeTypes.ImageWebp:
                        try
                        {
                            await _imageValidator.Validate(data.Data, data.ContentType);
                        }
                        catch (ImageException e)
                        {
                            throw new TimelinePostCreateDataException(index, "Image validation failed.", e);
                        }
                        break;
                    case MimeTypes.TextPlain:
                    case MimeTypes.TextMarkdown:
                        try
                        {
                            new UTF8Encoding(false, true).GetString(data.Data);
                        }
                        catch (DecoderFallbackException e)
                        {
                            throw new TimelinePostCreateDataException(index, "Text is not a valid utf-8 sequence.", e);
                        }
                        break;
                    default:
                        throw new TimelinePostCreateDataException(index, "Unsupported content type.");
                }
            }
            request.Time = request.Time?.MyToUtc();
            await CheckTimelineExistence(timelineId);
            await CheckUserExistence(authorId);
            var currentTime = _clock.GetCurrentTime();
            var finalTime = request.Time ?? currentTime;
            await using var transaction = await _database.Database.BeginTransactionAsync();
            var postEntity = new TimelinePostEntity
            {
                AuthorId = authorId,
                TimelineId = timelineId,
                Time = finalTime,
                LastUpdated = currentTime,
                Color = request.Color
            };
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            timelineEntity.CurrentPostLocalId += 1;
            postEntity.LocalId = timelineEntity.CurrentPostLocalId;
            _database.TimelinePosts.Add(postEntity);
            await _database.SaveChangesAsync();
            List dataTags = new List();
            for (int index = 0; index < request.DataList.Count; index++)
            {
                var data = request.DataList[index];
                var tag = await _dataManager.RetainEntry(data.Data);
                _database.TimelinePostData.Add(new TimelinePostDataEntity
                {
                    DataTag = tag,
                    Kind = data.ContentType,
                    Index = index,
                    PostId = postEntity.Id,
                    LastUpdated = currentTime,
                });
            }
            await _database.SaveChangesAsync();
            await transaction.CommitAsync();
            return postEntity;
        }
        public async Task PatchPost(long timelineId, long postId, TimelinePostPatchRequest request)
        {
            if (request is null)
                throw new ArgumentNullException(nameof(request));
            {
                if (!_colorValidator.Validate(request.Color, out var message))
                    throw new ArgumentException("Color is not valid.", nameof(request));
            }
            request.Time = request.Time?.MyToUtc();
            await CheckTimelineExistence(timelineId);
            var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
            if (entity is null)
                throw new TimelinePostNotExistException(timelineId, postId, false);
            if (entity.Deleted)
                throw new TimelinePostNotExistException(timelineId, postId, true);
            if (request.Time.HasValue)
                entity.Time = request.Time.Value;
            if (request.Color is not null)
                entity.Color = request.Color;
            entity.LastUpdated = _clock.GetCurrentTime();
            await _database.SaveChangesAsync();
            return entity;
        }
        public async Task DeletePost(long timelineId, long postId)
        {
            await CheckTimelineExistence(timelineId);
            var entity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
            if (entity == null)
                throw new TimelinePostNotExistException(timelineId, postId, false);
            if (entity.Deleted)
                throw new TimelinePostNotExistException(timelineId, postId, true);
            await using var transaction = await _database.Database.BeginTransactionAsync();
            entity.Deleted = true;
            entity.LastUpdated = _clock.GetCurrentTime();
            var dataEntities = await _database.TimelinePostData.Where(d => d.PostId == entity.Id).ToListAsync();
            foreach (var dataEntity in dataEntities)
            {
                await _dataManager.FreeEntry(dataEntity.DataTag);
            }
            _database.TimelinePostData.RemoveRange(dataEntities);
            await _database.SaveChangesAsync();
            await transaction.CommitAsync();
        }
        public async Task DeleteAllPostsOfUser(long userId)
        {
            var postEntities = await _database.TimelinePosts.Where(p => p.AuthorId == userId).Select(p => new { p.TimelineId, p.LocalId }).ToListAsync();
            foreach (var postEntity in postEntities)
            {
                await this.DeletePost(postEntity.TimelineId, postEntity.LocalId);
            }
        }
        public async Task HasPostModifyPermission(long timelineId, long postId, long modifierId, bool throwOnPostNotExist = false)
        {
            await CheckTimelineExistence(timelineId);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
            var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Deleted, p.AuthorId }).SingleOrDefaultAsync();
            if (postEntity is null)
            {
                if (throwOnPostNotExist)
                    throw new TimelinePostNotExistException(timelineId, postId, false);
                else
                    return true;
            }
            if (postEntity.Deleted && throwOnPostNotExist)
            {
                throw new TimelinePostNotExistException(timelineId, postId, true);
            }
            return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
        }
    }
}