using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SixLabors.ImageSharp;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Helpers;
using Timeline.Models;
using Timeline.Models.Validation;
using Timeline.Services.Exceptions;
using static Timeline.Resources.Services.TimelineService;
namespace Timeline.Services
{
    public static class TimelineHelper
    {
        public static string ExtractTimelineName(string name, out bool isPersonal)
        {
            if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase))
            {
                isPersonal = true;
                return name.Substring(1);
            }
            else
            {
                isPersonal = false;
                return name;
            }
        }
    }
    public enum TimelineUserRelationshipType
    {
        Own = 0b1,
        Join = 0b10,
        Default = Own | Join
    }
    public class TimelineUserRelationship
    {
        public TimelineUserRelationship(TimelineUserRelationshipType type, long userId)
        {
            Type = type;
            UserId = userId;
        }
        public TimelineUserRelationshipType Type { get; set; }
        public long UserId { get; set; }
    }
    public class PostData : ICacheableData
    {
#pragma warning disable CA1819 // Properties should not return arrays
        public byte[] Data { get; set; } = default!;
#pragma warning restore CA1819 // Properties should not return arrays
        public string Type { get; set; } = default!;
        public string ETag { get; set; } = default!;
        public DateTime? LastModified { get; set; } // TODO: Why nullable?
    }
    /// 
    /// This define the interface of both personal timeline and ordinary timeline.
    /// 
    public interface ITimelineService
    {
        /// 
        /// Get the timeline last modified time (not include name change).
        /// 
        /// The name of the timeline.
        /// The timeline info.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        Task GetTimelineLastModifiedTime(string timelineName);
        /// 
        /// Get the timeline unique id.
        /// 
        /// The name of the timeline.
        /// The timeline info.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        Task GetTimelineUniqueId(string timelineName);
        /// 
        /// Get the timeline info.
        /// 
        /// The name of the timeline.
        /// The timeline info.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        Task GetTimeline(string timelineName);
        /// 
        /// Set the properties of a timeline. 
        /// 
        /// The name of the timeline.
        /// The new properties. Null member means not to change.
        /// Thrown when  or  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties);
        /// 
        /// Get all the posts in the timeline.
        /// 
        /// The name of the timeline.
        /// The time that posts have been modified since.
        /// Whether include deleted posts.
        /// A list of all posts.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false);
        /// 
        /// Get the etag of data of a post.
        /// 
        /// The name of the timeline of the post.
        /// The id of the post.
        /// The etag of the data.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// Thrown when post of  does not exist or has been deleted.
        /// Thrown when post has no data.
        /// 
        Task GetPostDataETag(string timelineName, long postId);
        /// 
        /// Get the data of a post.
        /// 
        /// The name of the timeline of the post.
        /// The id of the post.
        /// The etag of the data.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// Thrown when post of  does not exist or has been deleted.
        /// Thrown when post has no data.
        /// 
        Task GetPostData(string timelineName, long postId);
        /// 
        /// Create a new text post in timeline.
        /// 
        /// The name of the timeline to create post against.
        /// The author's user id.
        /// The content text.
        /// The time of the post. If null, then current time is used.
        /// The info of the created post.
        /// Thrown when  or  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// Thrown if user of  does not exist.
        Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time);
        /// 
        /// Create a new image post in timeline.
        /// 
        /// The name of the timeline to create post against.
        /// The author's user id.
        /// The image data.
        /// The time of the post. If null, then use current time.
        /// The info of the created post.
        /// Thrown when  or  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// Thrown if user of  does not exist.
        /// Thrown if data is not a image. Validated by .
        Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time);
        /// 
        /// Delete a post.
        /// 
        /// The name of the timeline to delete post against.
        /// The id of the post to delete.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// Thrown when the post with given id does not exist or is deleted already.
        /// 
        /// First use  to check the permission.
        /// 
        Task DeletePost(string timelineName, long postId);
        /// 
        /// Change member of timeline.
        /// 
        /// The name of the timeline.
        /// A list of usernames of members to add. May be null.
        /// A list of usernames of members to remove. May be null.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// Thrown when names in  or  is not a valid username.
        /// Thrown when one of the user to change does not exist.
        /// 
        /// Operating on a username that is of bad format or does not exist always throws.
        /// Add a user that already is a member has no effects.
        /// Remove a user that is not a member also has not effects.
        /// Add and remove an identical user results in no effects.
        /// More than one same usernames are regarded as one.
        /// 
        Task ChangeMember(string timelineName, IList? membersToAdd, IList? membersToRemove);
        /// 
        /// Check whether a user can manage(change timeline info, member, ...) a timeline.
        /// 
        /// The name of the timeline.
        /// The id of the user to check on.
        /// True if the user can manage the timeline, otherwise false.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// This method does not check whether visitor is administrator.
        /// Return false if user with user id does not exist.
        /// 
        Task HasManagePermission(string timelineName, long userId);
        /// 
        /// Verify whether a visitor has the permission to read a timeline.
        /// 
        /// The name of the timeline.
        /// The id of the user to check on. Null means visitor without account.
        /// True if can read, false if can't read.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// This method does not check whether visitor is administrator.
        /// Return false if user with visitor id does not exist.
        /// 
        Task HasReadPermission(string timelineName, long? visitorId);
        /// 
        /// Verify whether a user has the permission to modify a post.
        /// 
        /// The name 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  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// 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(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false);
        /// 
        /// Verify whether a user is member of a timeline.
        /// 
        /// The name of the timeline.
        /// The id of user to check on.
        /// True if it is a member, false if not.
        /// Thrown when  is null.
        /// Throw when  is of bad format.
        /// 
        /// Thrown when timeline with name  does not exist.
        /// If it is a personal timeline, then inner exception is .
        /// 
        /// 
        /// Timeline owner is also considered as a member.
        /// Return false when user with user id does not exist.
        /// 
        Task IsMemberOf(string timelineName, long userId);
        /// 
        /// Get all timelines including personal and ordinary timelines.
        /// 
        /// Filter timelines related (own or is a member) to specific user.
        /// Filter timelines with given visibility. If null or empty, all visibilities are returned. Duplicate value are ignored.
        /// The list of timelines.
        /// 
        /// If user with related user id does not exist, empty list will be returned.
        /// 
        Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null);
        /// 
        /// Create a timeline.
        /// 
        /// The name of the timeline.
        /// The id of owner of the timeline.
        /// The info of the new timeline.
        /// Thrown when  is null.
        /// Thrown when timeline name is invalid.
        /// Thrown when the timeline already exists.
        /// Thrown when the owner user does not exist.
        Task CreateTimeline(string timelineName, long ownerId);
        /// 
        /// Delete a timeline.
        /// 
        /// The name of the timeline to delete.
        /// Thrown when  is null.
        /// Thrown when timeline name is invalid.
        /// Thrown when the timeline does not exist.
        Task DeleteTimeline(string timelineName);
    }
    public class TimelineService : ITimelineService
    {
        public TimelineService(ILogger logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock)
        {
            _logger = logger;
            _database = database;
            _dataManager = dataManager;
            _userService = userService;
            _imageValidator = imageValidator;
            _clock = clock;
        }
        private readonly ILogger _logger;
        private readonly DatabaseContext _database;
        private readonly IDataManager _dataManager;
        private readonly IUserService _userService;
        private readonly IImageValidator _imageValidator;
        private readonly IClock _clock;
        private readonly UsernameValidator _usernameValidator = new UsernameValidator();
        private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
        private void ValidateTimelineName(string name, string paramName)
        {
            if (!_timelineNameValidator.Validate(name, out var message))
            {
                throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName);
            }
        }
        private async Task MapTimelineFromEntity(TimelineEntity entity)
        {
            var owner = await _userService.GetUserById(entity.OwnerId);
            var members = new List();
            foreach (var memberEntity in entity.Members)
            {
                members.Add(await _userService.GetUserById(memberEntity.UserId));
            }
            return new Models.Timeline
            {
                UniqueID = entity.UniqueId,
                Name = entity.Name ?? ("@" + owner.Username),
                NameLastModified = entity.NameLastModified,
                Description = entity.Description ?? "",
                Owner = owner,
                Visibility = entity.Visibility,
                Members = members,
                CreateTime = entity.CreateTime,
                LastModified = entity.LastModified
            };
        }
        private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName)
        {
            var author = await _userService.GetUserById(entity.AuthorId);
            ITimelinePostContent? content = null;
            if (entity.Content != null)
            {
                var type = entity.ContentType;
                content = type switch
                {
                    TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content),
                    TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content),
                    _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type))
                };
            }
            return new TimelinePost(
                    id: entity.LocalId,
                    author: author,
                    content: content,
                    time: entity.Time,
                    lastUpdated: entity.LastUpdated,
                    timelineName: timelineName
                );
        }
        private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId)
        {
            var currentTime = _clock.GetCurrentTime();
            return new TimelineEntity
            {
                Name = name,
                NameLastModified = currentTime,
                OwnerId = ownerId,
                Visibility = TimelineVisibility.Register,
                CreateTime = currentTime,
                LastModified = currentTime,
                CurrentPostLocalId = 0,
                Members = new List()
            };
        }
        // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created.
        //
        // This method will check the name format and if it is invalid, ArgumentException is thrown.
        //
        // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception.
        // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown.
        //
        // It follows all timeline-related function common interface contracts.
        private async Task FindTimelineId(string timelineName)
        {
            timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal);
            if (isPersonal)
            {
                long userId;
                try
                {
                    userId = await _userService.GetUserIdByUsername(timelineName);
                }
                catch (ArgumentException e)
                {
                    throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e);
                }
                catch (UserNotExistException e)
                {
                    throw new TimelineNotExistException(timelineName, e);
                }
                var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync();
                if (timelineEntity != null)
                {
                    return timelineEntity.Id;
                }
                else
                {
                    var newTimelineEntity = CreateNewTimelineEntity(null, userId);
                    _database.Timelines.Add(newTimelineEntity);
                    await _database.SaveChangesAsync();
                    return newTimelineEntity.Id;
                }
            }
            else
            {
                if (timelineName == null)
                    throw new ArgumentNullException(nameof(timelineName));
                ValidateTimelineName(timelineName, nameof(timelineName));
                var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync();
                if (timelineEntity == null)
                {
                    throw new TimelineNotExistException(timelineName);
                }
                else
                {
                    return timelineEntity.Id;
                }
            }
        }
        public async Task GetTimelineLastModifiedTime(string timelineName)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync();
            return timelineEntity.LastModified;
        }
        public async Task GetTimelineUniqueId(string timelineName)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync();
            return timelineEntity.UniqueId;
        }
        public async Task GetTimeline(string timelineName)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync();
            return await MapTimelineFromEntity(timelineEntity);
        }
        public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var query = _database.TimelinePosts.OrderBy(p => p.Time).Where(p => p.TimelineId == timelineId);
            if (!includeDeleted)
            {
                query = query.Where(p => p.Content != null);
            }
            if (modifiedSince.HasValue)
            {
                query = query.Where(p => p.LastUpdated >= modifiedSince);
            }
            var postEntities = await query.ToListAsync();
            var posts = new List();
            foreach (var entity in postEntities)
            {
                posts.Add(await MapTimelinePostFromEntity(entity, timelineName));
            }
            return posts;
        }
        public async Task GetPostDataETag(string timelineName, long postId)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
            if (postEntity == null)
                throw new TimelinePostNotExistException(timelineName, postId, false);
            if (postEntity.Content == null)
                throw new TimelinePostNotExistException(timelineName, postId, true);
            if (postEntity.ContentType != TimelinePostContentTypes.Image)
                throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
            var tag = postEntity.Content;
            return tag;
        }
        public async Task GetPostData(string timelineName, long postId)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync();
            if (postEntity == null)
                throw new TimelinePostNotExistException(timelineName, postId, false);
            if (postEntity.Content == null)
                throw new TimelinePostNotExistException(timelineName, postId, true);
            if (postEntity.ContentType != TimelinePostContentTypes.Image)
                throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost);
            var tag = postEntity.Content;
            byte[] data;
            try
            {
                data = await _dataManager.GetEntry(tag);
            }
            catch (InvalidOperationException e)
            {
                throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e);
            }
            if (postEntity.ExtraContent == null)
            {
                _logger.LogWarning(LogGetDataNoFormat);
                var format = Image.DetectFormat(data);
                postEntity.ExtraContent = format.DefaultMimeType;
                await _database.SaveChangesAsync();
            }
            return new PostData
            {
                Data = data,
                Type = postEntity.ExtraContent,
                ETag = tag,
                LastModified = postEntity.LastUpdated
            };
        }
        public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            if (text == null)
                throw new ArgumentNullException(nameof(text));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            var author = await _userService.GetUserById(authorId);
            var currentTime = _clock.GetCurrentTime();
            var finalTime = time ?? currentTime;
            timelineEntity.CurrentPostLocalId += 1;
            var postEntity = new TimelinePostEntity
            {
                LocalId = timelineEntity.CurrentPostLocalId,
                ContentType = TimelinePostContentTypes.Text,
                Content = text,
                AuthorId = authorId,
                TimelineId = timelineId,
                Time = finalTime,
                LastUpdated = currentTime
            };
            _database.TimelinePosts.Add(postEntity);
            await _database.SaveChangesAsync();
            return new TimelinePost(
                id: postEntity.LocalId,
                content: new TextTimelinePostContent(text),
                time: finalTime,
                author: author,
                lastUpdated: currentTime,
                timelineName: timelineName
            );
        }
        public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            if (data == null)
                throw new ArgumentNullException(nameof(data));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            var author = await _userService.GetUserById(authorId);
            var imageFormat = await _imageValidator.Validate(data);
            var imageFormatText = imageFormat.DefaultMimeType;
            var tag = await _dataManager.RetainEntry(data);
            var currentTime = _clock.GetCurrentTime();
            var finalTime = time ?? currentTime;
            timelineEntity.CurrentPostLocalId += 1;
            var postEntity = new TimelinePostEntity
            {
                LocalId = timelineEntity.CurrentPostLocalId,
                ContentType = TimelinePostContentTypes.Image,
                Content = tag,
                ExtraContent = imageFormatText,
                AuthorId = authorId,
                TimelineId = timelineId,
                Time = finalTime,
                LastUpdated = currentTime
            };
            _database.TimelinePosts.Add(postEntity);
            await _database.SaveChangesAsync();
            return new TimelinePost(
                id: postEntity.LocalId,
                content: new ImageTimelinePostContent(tag),
                time: finalTime,
                author: author,
                lastUpdated: currentTime,
                timelineName: timelineName
            );
        }
        public async Task DeletePost(string timelineName, long id)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync();
            if (post == null)
                throw new TimelinePostNotExistException(timelineName, id, false);
            if (post.Content == null)
                throw new TimelinePostNotExistException(timelineName, id, true);
            string? dataTag = null;
            if (post.ContentType == TimelinePostContentTypes.Image)
            {
                dataTag = post.Content;
            }
            post.Content = null;
            post.LastUpdated = _clock.GetCurrentTime();
            await _database.SaveChangesAsync();
            if (dataTag != null)
            {
                await _dataManager.FreeEntry(dataTag);
            }
        }
        public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            if (newProperties == null)
                throw new ArgumentNullException(nameof(newProperties));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            var changed = false;
            if (newProperties.Description != null)
            {
                changed = true;
                timelineEntity.Description = newProperties.Description;
            }
            if (newProperties.Visibility.HasValue)
            {
                changed = true;
                timelineEntity.Visibility = newProperties.Visibility.Value;
            }
            if (changed)
            {
                var currentTime = _clock.GetCurrentTime();
                timelineEntity.LastModified = currentTime;
            }
            await _database.SaveChangesAsync();
        }
        public async Task ChangeMember(string timelineName, IList? add, IList? remove)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            List? RemoveDuplicateAndCheckFormat(IList? list, string paramName)
            {
                if (list != null)
                {
                    List result = new List();
                    var count = list.Count;
                    for (var index = 0; index < count; index++)
                    {
                        var username = list[index];
                        if (result.Contains(username))
                        {
                            continue;
                        }
                        var (validationResult, message) = _usernameValidator.Validate(username);
                        if (!validationResult)
                            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, ExceptionChangeMemberUsernameBadFormat, index), nameof(paramName));
                        result.Add(username);
                    }
                    return result;
                }
                else
                {
                    return null;
                }
            }
            var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, nameof(add));
            var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, nameof(remove));
            // remove those both in add and remove
            if (simplifiedAdd != null && simplifiedRemove != null)
            {
                var usersToClean = simplifiedRemove.Where(u => simplifiedAdd.Contains(u)).ToList();
                foreach (var u in usersToClean)
                {
                    simplifiedAdd.Remove(u);
                    simplifiedRemove.Remove(u);
                }
                if (simplifiedAdd.Count == 0)
                    simplifiedAdd = null;
                if (simplifiedRemove.Count == 0)
                    simplifiedRemove = null;
            }
            if (simplifiedAdd == null && simplifiedRemove == null)
                return;
            var timelineId = await FindTimelineId(timelineName);
            async Task?> CheckExistenceAndGetId(List? list)
            {
                if (list == null)
                    return null;
                List result = new List();
                foreach (var username in list)
                {
                    result.Add(await _userService.GetUserIdByUsername(username));
                }
                return result;
            }
            var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd);
            var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove);
            if (userIdsAdd != null)
            {
                var membersToAdd = userIdsAdd.Select(id => new TimelineMemberEntity { UserId = id, TimelineId = timelineId }).ToList();
                _database.TimelineMembers.AddRange(membersToAdd);
            }
            if (userIdsRemove != null)
            {
                var membersToRemove = await _database.TimelineMembers.Where(m => m.TimelineId == timelineId && userIdsRemove.Contains(m.UserId)).ToListAsync();
                _database.TimelineMembers.RemoveRange(membersToRemove);
            }
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            timelineEntity.LastModified = _clock.GetCurrentTime();
            await _database.SaveChangesAsync();
        }
        public async Task HasManagePermission(string timelineName, long userId)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
            return userId == timelineEntity.OwnerId;
        }
        public async Task HasReadPermission(string timelineName, long? visitorId)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync();
            if (timelineEntity.Visibility == TimelineVisibility.Public)
                return true;
            if (timelineEntity.Visibility == TimelineVisibility.Register && visitorId != null)
                return true;
            if (visitorId == null)
            {
                return false;
            }
            else
            {
                var memberEntity = await _database.TimelineMembers.Where(m => m.UserId == visitorId && m.TimelineId == timelineId).SingleOrDefaultAsync();
                return memberEntity != null;
            }
        }
        public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            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.Content, p.AuthorId }).SingleOrDefaultAsync();
            if (postEntity == null)
            {
                if (throwOnPostNotExist)
                    throw new TimelinePostNotExistException(timelineName, postId, false);
                else
                    return true;
            }
            if (postEntity.Content == null && throwOnPostNotExist)
            {
                throw new TimelinePostNotExistException(timelineName, postId, true);
            }
            return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId;
        }
        public async Task IsMemberOf(string timelineName, long userId)
        {
            if (timelineName == null)
                throw new ArgumentNullException(nameof(timelineName));
            var timelineId = await FindTimelineId(timelineName);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
            if (userId == timelineEntity.OwnerId)
                return true;
            return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId);
        }
        public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null)
        {
            List entities;
            IQueryable ApplyTimelineVisibilityFilter(IQueryable query)
            {
                if (visibility != null && visibility.Count != 0)
                {
                    return query.Where(t => visibility.Contains(t.Visibility));
                }
                return query;
            }
            bool allVisibilities = visibility == null || visibility.Count == 0;
            if (relate == null)
            {
                entities = await ApplyTimelineVisibilityFilter(_database.Timelines).Include(t => t.Members).ToListAsync();
            }
            else
            {
                entities = new List();
                if ((relate.Type & TimelineUserRelationshipType.Own) != 0)
                {
                    entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).Include(t => t.Members).ToListAsync());
                }
                if ((relate.Type & TimelineUserRelationshipType.Join) != 0)
                {
                    entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).ThenInclude(t => t.Members).Select(m => m.Timeline)).ToListAsync());
                }
            }
            var result = new List();
            foreach (var entity in entities)
            {
                result.Add(await MapTimelineFromEntity(entity));
            }
            return result;
        }
        public async Task CreateTimeline(string name, long owner)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            ValidateTimelineName(name, nameof(name));
            var user = await _userService.GetUserById(owner);
            var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
            if (conflict)
                throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
            var newEntity = CreateNewTimelineEntity(name, user.Id!.Value);
            _database.Timelines.Add(newEntity);
            await _database.SaveChangesAsync();
            return await MapTimelineFromEntity(newEntity);
        }
        public async Task DeleteTimeline(string name)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            ValidateTimelineName(name, nameof(name));
            var entity = await _database.Timelines.Where(t => t.Name == name).SingleOrDefaultAsync();
            if (entity == null)
                throw new TimelineNotExistException(name);
            _database.Timelines.Remove(entity);
            await _database.SaveChangesAsync();
        }
    }
}