using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Models;
using Timeline.Models.Http;
using Timeline.Models.Validation;
namespace Timeline.Services
{
    /// 
    /// This define the common interface of both personal timeline
    /// and normal timeline.
    /// 
    /// 
    /// The "name" parameter in method means name of timeline in
    ///  while username of the owner
    /// of the personal timeline in .
    /// 
    public interface IBaseTimelineService
    {
        /// 
        /// Get all the posts in the timeline.
        /// 
        /// Username or the timeline name. See remarks of .
        /// A list of all posts.
        /// Thrown when  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        Task> GetPosts(string name);
        /// 
        /// Create a new post in timeline.
        /// 
        /// Username or the timeline name. See remarks of .
        /// The author's username.
        /// The content.
        /// The time of the post. If null, then use current time.
        /// The info of the created post.
        /// Thrown when  or  or  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        /// Thrown if  is of bad format.
        /// Thrown if  does not exist.
        Task CreatePost(string name, string author, string content, DateTime? time);
        /// 
        /// Delete a post
        /// 
        /// Username or the timeline name. See remarks of .
        /// The id of the post to delete.
        /// Thrown when  or  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        /// 
        /// Thrown when the post with given id does not exist or is deleted already.
        /// 
        /// 
        /// First use 
        /// to check the permission.
        /// 
        Task DeletePost(string name, long id);
        /// 
        /// Set the properties of a timeline. 
        /// 
        /// Username or the timeline name. See remarks of .
        /// The new properties. Null member means not to change.
        /// Thrown when  or  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties);
        /// 
        /// Remove members to a timeline.
        /// 
        /// Username or the timeline name. See remarks of .
        /// 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.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        /// 
        /// Thrown when an exception occurs on the user list.
        /// The inner exception is 
        /// when one of the username is invalid.
        /// The inner exception is 
        /// 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 name, IList? add, IList? remove);
        /// 
        /// Verify whether a visitor has the permission to read a timeline.
        /// 
        /// Username or the timeline name. See remarks of .
        /// The user to check on. Null means visitor without account.
        /// True if can read, false if can't read.
        /// Thrown when  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        /// 
        /// Thrown when  is of bad format.
        /// 
        /// 
        /// Thrown when  does not exist.
        /// 
        Task HasReadPermission(string name, string? username);
        /// 
        /// Verify whether a user has the permission to modify a post.
        /// 
        /// Username or the timeline name. See remarks of .
        /// The user to check on.
        /// True if can modify, false if can't modify.
        /// Thrown when  or  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        /// 
        /// Thrown when the post with given id does not exist or is deleted already.
        /// 
        /// 
        /// Thrown when  is of bad format.
        /// 
        /// 
        /// Thrown when  does not exist.
        /// 
        /// 
        /// 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.
        /// 
        Task HasPostModifyPermission(string name, long id, string username);
        /// 
        /// Verify whether a user is member of a timeline.
        /// 
        /// Username or the timeline name. See remarks of .
        /// The user to check on.
        /// True if it is a member, false if not.
        /// Thrown when  or  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        /// 
        /// Thrown when  is not a valid username.
        /// 
        /// 
        /// Thrown when user  does not exist.
        /// 
        /// 
        /// Timeline owner is also considered as a member.
        /// 
        Task IsMemberOf(string name, string username);
    }
    /// 
    /// Service for normal timeline.
    /// 
    public interface ITimelineService : IBaseTimelineService
    {
        /// 
        /// Get the timeline info.
        /// 
        /// The name of the timeline.
        /// The timeline info.
        /// Thrown when  is null.
        /// 
        /// Thrown when timeline name is invalid. Currently it means it is an empty string.
        /// 
        /// 
        /// Thrown when timeline with the name does not exist.
        /// 
        Task GetTimeline(string name);
        /// 
        /// Create a timeline.
        /// 
        /// The name of the timeline.
        /// The owner of the timeline.
        /// Thrown when  or  is null.
        /// 
        /// Thrown when timeline name is invalid. Currently it means it is an empty string.
        /// 
        /// 
        /// Thrown when the timeline already exists.
        /// 
        /// 
        /// Thrown when the username of the owner is not valid.
        /// 
        /// 
        /// Thrown when the owner user does not exist.
        Task CreateTimeline(string name, string owner);
    }
    public interface IPersonalTimelineService : IBaseTimelineService
    {
        /// 
        /// Get the timeline info.
        /// 
        /// The username of the owner of the personal timeline.
        /// The timeline info.
        /// 
        /// Thrown when  is null.
        /// 
        /// 
        /// Thrown when  is of bad format. Inner exception MUST be .
        /// 
        /// 
        /// Thrown when the user does not exist. Inner exception MUST be .
        /// 
        Task GetTimeline(string username);
    }
    public abstract class BaseTimelineService : IBaseTimelineService
    {
        protected BaseTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
        {
            Clock = clock;
            Database = database;
        }
        protected IClock Clock { get; }
        protected UsernameValidator UsernameValidator { get; } = new UsernameValidator();
        protected DatabaseContext Database { get; }
        /// 
        /// Find the timeline id by the name.
        /// For details, see remarks.
        /// 
        /// The username or the timeline name. See remarks.
        /// The id of the timeline entity.
        /// Thrown when  is null.
        /// 
        /// Thrown when timeline name is of bad format.
        /// For normal timeline, it means name is an empty string.
        /// For personal timeline, it means the username is of bad format,
        /// the inner exception should be a .
        /// 
        /// 
        /// Thrown when timeline does not exist.
        /// For normal timeline, it means the name does not exist.
        /// For personal timeline, it means the user of that username does not exist
        /// and the inner exception should be a .
        /// 
        /// 
        /// This is the common but different part for both types of timeline service.
        /// For class that implements , this method should
        /// find the timeline entity id by the given  as the username of the owner.
        /// For class that implements , this method should
        /// find the timeline entity id by the given  as the timeline name.
        /// This method should be called by many other method that follows the contract.
        /// 
        protected abstract Task FindTimelineId(string name);
        public async Task> GetPosts(string name)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            var timelineId = await FindTimelineId(name);
            var postEntities = await Database.TimelinePosts.Where(p => p.TimelineId == timelineId).ToListAsync();
            var posts = new List(await Task.WhenAll(postEntities.Select(async p => new TimelinePostInfo
            {
                Id = p.Id,
                Content = p.Content,
                Author = (await Database.Users.Where(u => u.Id == p.AuthorId).Select(u => new { u.Name }).SingleAsync()).Name,
                Time = p.Time
            })));
            return posts;
        }
        public async Task CreatePost(string name, string author, string content, DateTime? time)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (author == null)
                throw new ArgumentNullException(nameof(author));
            if (content == null)
                throw new ArgumentNullException(nameof(content));
            {
                var (result, message) = UsernameValidator.Validate(author);
                if (!result)
                {
                    throw new UsernameBadFormatException(author, message);
                }
            }
            var timelineId = await FindTimelineId(name);
            var authorEntity = Database.Users.Where(u => u.Name == author).Select(u => new { u.Id }).SingleOrDefault();
            if (authorEntity == null)
            {
                throw new UserNotExistException(author);
            }
            var authorId = authorEntity.Id;
            var currentTime = Clock.GetCurrentTime();
            var postEntity = new TimelinePostEntity
            {
                Content = content,
                AuthorId = authorId,
                TimelineId = timelineId,
                Time = time ?? currentTime,
                LastUpdated = currentTime
            };
            Database.TimelinePosts.Add(postEntity);
            await Database.SaveChangesAsync();
            return new TimelinePostCreateResponse
            {
                Id = postEntity.Id,
                Time = postEntity.Time
            };
        }
        public async Task DeletePost(string name, long id)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            var timelineId = FindTimelineId(name);
            var post = await Database.TimelinePosts.Where(p => p.Id == id).SingleOrDefaultAsync();
            if (post == null)
                throw new TimelinePostNotExistException(id);
            Database.TimelinePosts.Remove(post);
            await Database.SaveChangesAsync();
        }
        public async Task ChangeProperty(string name, TimelinePropertyChangeRequest newProperties)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (newProperties == null)
                throw new ArgumentNullException(nameof(newProperties));
            var timelineId = await FindTimelineId(name);
            var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            if (newProperties.Description != null)
            {
                timelineEntity.Description = newProperties.Description;
            }
            if (newProperties.Visibility.HasValue)
            {
                timelineEntity.Visibility = newProperties.Visibility.Value;
            }
            await Database.SaveChangesAsync();
        }
        public async Task ChangeMember(string name, IList? add, IList? remove)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            // remove duplication and check the format of each username.
            // Return a username->index map.
            Dictionary? RemoveDuplicateAndCheckFormat(IList? list, TimelineMemberOperationUserException.MemberOperation operation)
            {
                if (list != null)
                {
                    Dictionary result = new Dictionary();
                    var count = list.Count;
                    for (var index = 0; index < count; index++)
                    {
                        var username = list[index];
                        if (result.ContainsKey(username))
                        {
                            continue;
                        }
                        var (validationResult, message) = UsernameValidator.Validate(username);
                        if (!validationResult)
                            throw new TimelineMemberOperationUserException(
                                index, operation, username,
                                new UsernameBadFormatException(username, message));
                        result.Add(username, index);
                    }
                    return result;
                }
                else
                {
                    return null;
                }
            }
            var simplifiedAdd = RemoveDuplicateAndCheckFormat(add, TimelineMemberOperationUserException.MemberOperation.Add);
            var simplifiedRemove = RemoveDuplicateAndCheckFormat(remove, TimelineMemberOperationUserException.MemberOperation.Remove);
            // remove those both in add and remove
            if (simplifiedAdd != null && simplifiedRemove != null)
            {
                var usersToClean = simplifiedRemove.Keys.Where(u => simplifiedAdd.ContainsKey(u));
                foreach (var u in usersToClean)
                {
                    simplifiedAdd.Remove(u);
                    simplifiedRemove.Remove(u);
                }
            }
            var timelineId = await FindTimelineId(name);
            async Task?> CheckExistenceAndGetId(Dictionary? map, TimelineMemberOperationUserException.MemberOperation operation)
            {
                if (map == null)
                    return null;
                List result = new List();
                foreach (var (username, index) in map)
                {
                    var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
                    if (user == null)
                    {
                        throw new TimelineMemberOperationUserException(index, operation, username,
                            new UserNotExistException(username));
                    }
                    result.Add(user.Id);
                }
                return result;
            }
            var userIdsAdd = await CheckExistenceAndGetId(simplifiedAdd, TimelineMemberOperationUserException.MemberOperation.Add);
            var userIdsRemove = await CheckExistenceAndGetId(simplifiedRemove, TimelineMemberOperationUserException.MemberOperation.Remove);
            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);
            }
            await Database.SaveChangesAsync();
        }
        public async Task HasReadPermission(string name, string? username)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            long? userId = null;
            if (username != null)
            {
                var (result, message) = UsernameValidator.Validate(username);
                if (!result)
                {
                    throw new UsernameBadFormatException(username);
                }
                var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
                if (user == null)
                {
                    throw new UserNotExistException(username);
                }
                userId = user.Id;
            }
            var timelineId = await FindTimelineId(name);
            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 && username != null)
                return true;
            if (userId == null)
            {
                return false;
            }
            else
            {
                var memberEntity = await Database.TimelineMembers.Where(m => m.UserId == userId && m.TimelineId == timelineId).SingleOrDefaultAsync();
                return memberEntity != null;
            }
        }
        public async Task HasPostModifyPermission(string name, long id, string username)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            {
                var (result, message) = UsernameValidator.Validate(username);
                if (!result)
                {
                    throw new UsernameBadFormatException(username);
                }
            }
            var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
            if (user == null)
            {
                throw new UserNotExistException(username);
            }
            var userId = user.Id;
            var timelineId = await FindTimelineId(name);
            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 == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync();
            if (postEntity == null)
                throw new TimelinePostNotExistException(id);
            return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId;
        }
        public async Task IsMemberOf(string name, string username)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            {
                var (result, message) = UsernameValidator.Validate(username);
                if (!result)
                {
                    throw new UsernameBadFormatException(username);
                }
            }
            var user = await Database.Users.Where(u => u.Name == username).Select(u => new { u.Id }).SingleOrDefaultAsync();
            if (user == null)
            {
                throw new UserNotExistException(username);
            }
            var userId = user.Id;
            var timelineId = await FindTimelineId(name);
            var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync();
            if (userId == timelineEntity.OwnerId)
                return true;
            var timelineMemberEntity = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId && m.UserId == userId).SingleOrDefaultAsync();
            return timelineMemberEntity != null;
        }
    }
    public class PersonalTimelineService : BaseTimelineService, IPersonalTimelineService
    {
        public PersonalTimelineService(ILoggerFactory loggerFactory, DatabaseContext database, IClock clock)
            : base(loggerFactory, database, clock)
        {
        }
        protected override async Task FindTimelineId(string name)
        {
            {
                var (result, message) = UsernameValidator.Validate(name);
                if (!result)
                {
                    throw new TimelineNameBadFormatException(name, new UsernameBadFormatException(name, message));
                }
            }
            var userEntity = await Database.Users.Where(u => u.Name == name).Select(u => new { u.Id }).SingleOrDefaultAsync();
            if (userEntity == null)
            {
                throw new TimelineNotExistException(name, new UserNotExistException(name));
            }
            var userId = userEntity.Id;
            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 = new TimelineEntity
                {
                    Name = null,
                    Description = null,
                    OwnerId = userId,
                    Visibility = TimelineVisibility.Register,
                    CreateTime = Clock.GetCurrentTime(),
                };
                Database.Timelines.Add(newTimelineEntity);
                await Database.SaveChangesAsync();
                return newTimelineEntity.Id;
            }
        }
        public async Task GetTimeline(string username)
        {
            if (username == null)
                throw new ArgumentNullException(nameof(username));
            var timelineId = await FindTimelineId(username);
            var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            var timelineMemberEntities = await Database.TimelineMembers.Where(m => m.TimelineId == timelineId).Select(m => new { m.UserId }).ToListAsync();
            var memberUsernameTasks = timelineMemberEntities.Select(m => Database.Users.Where(u => u.Id == m.UserId).Select(u => u.Name).SingleAsync()).ToArray();
            var memberUsernames = await Task.WhenAll(memberUsernameTasks);
            return new BaseTimelineInfo
            {
                Description = timelineEntity.Description ?? "",
                Owner = username,
                Visibility = timelineEntity.Visibility,
                Members = memberUsernames.ToList()
            };
        }
    }
}