using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Timeline.Entities;
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[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 TimelineChangePropertyParams
    {
        public string? Name { get; set; }
        public string? Title { get; set; }
        public string? Description { get; set; }
        public TimelineVisibility? Visibility { get; set; }
        public string? Color { get; set; }
    }
    /// 
    /// This define the interface of both personal timeline and ordinary timeline.
    /// 
    public interface ITimelineService : IBasicTimelineService
    {
        /// 
        /// Get the timeline info.
        /// 
        /// Id of timeline.
        /// The timeline info.
        /// Thrown when timeline does not exist.
        Task GetTimeline(long id);
        /// 
        /// Set the properties of a timeline. 
        /// 
        /// The id of the timeline.
        /// The new properties. Null member means not to change.
        /// Thrown when  is null.
        /// Thrown when timeline with given id does not exist.
        /// Thrown when a timeline with new name already exists.
        Task ChangeProperty(long id, TimelineChangePropertyParams newProperties);
        /// 
        /// Add a member to timeline.
        /// 
        /// Timeline id.
        /// User id.
        /// True if the memeber was added. False if it is already a member.
        /// Thrown when timeline does not exist.
        /// Thrown when the user does not exist.
        Task AddMember(long timelineId, long userId);
        /// 
        /// Remove a member from timeline.
        /// 
        /// Timeline id.
        /// User id.
        /// True if the memeber was removed. False if it was not a member before.
        /// Thrown when timeline does not exist.
        /// Thrown when the user does not exist.
        Task RemoveMember(long timelineId, long userId);
        /// 
        /// Check whether a user can manage(change timeline info, member, ...) a timeline.
        /// 
        /// The id of the timeline.
        /// The id of the user to check on.
        /// True if the user can manage the timeline, otherwise false.
        /// Thrown when timeline does not exist.
        /// 
        /// This method does not check whether visitor is administrator.
        /// Return false if user with user id does not exist.
        /// 
        Task HasManagePermission(long timelineId, long userId);
        /// 
        /// Verify whether a visitor has the permission to read a timeline.
        /// 
        /// The id 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 timeline does not exist.
        /// 
        /// This method does not check whether visitor is administrator.
        /// Return false if user with visitor id does not exist.
        /// 
        Task HasReadPermission(long timelineId, long? visitorId);
        /// 
        /// Verify whether a user is member of a timeline.
        /// 
        /// The id of the timeline.
        /// The id of user to check on.
        /// True if it is a member, false if not.
        /// Thrown when timeline does not exist.
        /// 
        /// Timeline owner is also considered as a member.
        /// Return false when user with user id does not exist.
        /// 
        Task IsMemberOf(long timelineId, 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 id of the timeline to delete.
        /// Thrown when the timeline does not exist.
        Task DeleteTimeline(long id);
    }
    public class TimelineService : BasicTimelineService, ITimelineService
    {
        public TimelineService(DatabaseContext database, IBasicUserService userService, IClock clock)
            : base(database, userService, clock)
        {
            _database = database;
            _userService = userService;
            _clock = clock;
        }
        private readonly DatabaseContext _database;
        private readonly IBasicUserService _userService;
        private readonly IClock _clock;
        private readonly TimelineNameValidator _timelineNameValidator = new TimelineNameValidator();
        private readonly ColorValidator _colorValidator = new ColorValidator();
        private void ValidateTimelineName(string name, string paramName)
        {
            if (!_timelineNameValidator.Validate(name, out var message))
            {
                throw new ArgumentException(ExceptionTimelineNameBadFormat.AppendAdditionalMessage(message), paramName);
            }
        }
        public async Task GetTimeline(long id)
        {
            var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
            if (entity is null)
                throw new TimelineNotExistException(id);
            return entity;
        }
        public async Task ChangeProperty(long id, TimelineChangePropertyParams newProperties)
        {
            if (newProperties is null)
                throw new ArgumentNullException(nameof(newProperties));
            if (newProperties.Name is not null)
                ValidateTimelineName(newProperties.Name, nameof(newProperties));
            if (newProperties.Color is not null)
            {
                var (result, message) = _colorValidator.Validate(newProperties.Color);
                if (!result)
                {
                    throw new ArgumentException(message, nameof(newProperties));
                }
            }
            var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
            if (entity is null)
                throw new TimelineNotExistException(id);
            var changed = false;
            var nameChanged = false;
            if (newProperties.Name is not null)
            {
                var conflict = await _database.Timelines.AnyAsync(t => t.Name == newProperties.Name);
                if (conflict)
                    throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
                entity.Name = newProperties.Name;
                changed = true;
                nameChanged = true;
            }
            if (newProperties.Title != null)
            {
                changed = true;
                entity.Title = newProperties.Title;
            }
            if (newProperties.Description != null)
            {
                changed = true;
                entity.Description = newProperties.Description;
            }
            if (newProperties.Visibility.HasValue)
            {
                changed = true;
                entity.Visibility = newProperties.Visibility.Value;
            }
            if (newProperties.Color is not null)
            {
                changed = true;
                entity.Color = newProperties.Color;
            }
            if (changed)
            {
                var currentTime = _clock.GetCurrentTime();
                entity.LastModified = currentTime;
                if (nameChanged)
                    entity.NameLastModified = currentTime;
            }
            await _database.SaveChangesAsync();
        }
        public async Task AddMember(long timelineId, long userId)
        {
            if (!await CheckExistence(timelineId))
                throw new TimelineNotExistException(timelineId);
            if (!await _userService.CheckUserExistence(userId))
                throw new UserNotExistException(userId);
            if (await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId))
                return false;
            var entity = new TimelineMemberEntity { UserId = userId, TimelineId = timelineId };
            _database.TimelineMembers.Add(entity);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            timelineEntity.LastModified = _clock.GetCurrentTime();
            await _database.SaveChangesAsync();
            return true;
        }
        public async Task RemoveMember(long timelineId, long userId)
        {
            if (!await CheckExistence(timelineId))
                throw new TimelineNotExistException(timelineId);
            if (!await _userService.CheckUserExistence(userId))
                throw new UserNotExistException(userId);
            var entity = await _database.TimelineMembers.SingleOrDefaultAsync(m => m.TimelineId == timelineId && m.UserId == userId);
            if (entity is null) return false;
            _database.TimelineMembers.Remove(entity);
            var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync();
            timelineEntity.LastModified = _clock.GetCurrentTime();
            await _database.SaveChangesAsync();
            return true;
        }
        public async Task HasManagePermission(long timelineId, long userId)
        {
            var entity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleOrDefaultAsync();
            if (entity is null)
                throw new TimelineNotExistException(timelineId);
            return entity.OwnerId == userId;
        }
        public async Task HasReadPermission(long timelineId, long? visitorId)
        {
            var entity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleOrDefaultAsync();
            if (entity is null)
                throw new TimelineNotExistException(timelineId);
            if (entity.Visibility == TimelineVisibility.Public)
                return true;
            if (entity.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 is not null;
            }
        }
        public async Task IsMemberOf(long timelineId, long userId)
        {
            var entity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleOrDefaultAsync();
            if (entity is null)
                throw new TimelineNotExistException(timelineId);
            if (userId == entity.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).ToListAsync();
            }
            else
            {
                entities = new List();
                if ((relate.Type & TimelineUserRelationshipType.Own) != 0)
                {
                    entities.AddRange(await ApplyTimelineVisibilityFilter(_database.Timelines.Where(t => t.OwnerId == relate.UserId)).ToListAsync());
                }
                if ((relate.Type & TimelineUserRelationshipType.Join) != 0)
                {
                    entities.AddRange(await ApplyTimelineVisibilityFilter(_database.TimelineMembers.Where(m => m.UserId == relate.UserId).Include(m => m.Timeline).Select(m => m.Timeline)).ToListAsync());
                }
            }
            return entities;
        }
        public async Task CreateTimeline(string name, long owner)
        {
            if (name == null)
                throw new ArgumentNullException(nameof(name));
            ValidateTimelineName(name, nameof(name));
            var conflict = await _database.Timelines.AnyAsync(t => t.Name == name);
            if (conflict)
                throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict);
            var entity = CreateNewTimelineEntity(name, owner);
            _database.Timelines.Add(entity);
            await _database.SaveChangesAsync();
            return entity;
        }
        public async Task DeleteTimeline(long id)
        {
            var entity = await _database.Timelines.Where(t => t.Id == id).SingleOrDefaultAsync();
            if (entity is null)
                throw new TimelineNotExistException(id);
            _database.Timelines.Remove(entity);
            await _database.SaveChangesAsync();
        }
    }
    public static class TimelineServiceExtensions
    {
        public static async Task> GetTimelineList(this ITimelineService service, IEnumerable ids)
        {
            var timelines = new List();
            foreach (var id in ids)
            {
                timelines.Add(await service.GetTimeline(id));
            }
            return timelines;
        }
    }
}