using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using SharpRSS.API.Contracts.DTOs.Groups;
using SharpRSS.API.Contracts.DTOs.Sessions;
using SharpRSS.API.Contracts.DTOs.Users;
using SharpRSS.API.Contracts.Models;
using SharpRSS.API.Cryptography;
using SharpRSS.API.Models;
using ToolQit;
using ToolQit.Extensions;
using ToolQit.Logging;

namespace SharpRSS.API.Data
{
    public class AuthService
    {
        public AuthService(IConfiguration configuration)
        {
            _configuration = configuration;
            _log = LogManager.CreateLogger(typeof(AuthService));
            _log.Information("Setting up service...");
        }

        private readonly IConfiguration _configuration;
        private readonly ILog _log;

        public async Task<ResultOr<Session>> Authenticate(AuthenticateUser authUser)
        {
            if (authUser.UserName.IsNullEmptyWhiteSpace() || authUser.Password.IsNullEmptyWhiteSpace())
                return ResultOr<Session>.Failed("Username/Password cannot be empty!");
            try
            {
                DbAccess access = new DbAccess(_configuration);
                var user = await access.Users.Where(u => u.Uid == authUser.UserName).FirstOrDefaultAsync();
                if (user == null)
                    return ResultOr<Session>.Failed("Unknown user");
                if (!user.Active) return ResultOr<Session>.Failed("User is not active, contact your administrator to enable this account!");
                if (!Hasher.ComparePasswords(authUser.Password, user.PswHash, user.Salt))
                    return ResultOr<Session>.Failed($"Failed to authenticate user: '{user.Uid}'");
                DbSession session = DbSession.CreateSession(user.Uid);
                access.Sessions.Add(session);
                bool saved = await access.SaveChangesAsync() > 0;
                return saved ? ResultOr<Session>.Ok(Utilities.ConvertFrom<Session, DbSession>(session)) : ResultOr<Session>.Failed($"Could create session for user: '{user.Uid}'");
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return ResultOr<Session>.Failed(e.Message, ResultStatus.InternalFail);
            }
        }

        internal async Task<ResultOr<AuthorizationSet>> ValidateSession(string sessionId)
        {
            if (sessionId.IsNullEmptyWhiteSpace()) return ResultOr<AuthorizationSet>.Failed("Session ID is empty!");
            try
            {
                DbAccess access = new DbAccess(_configuration);
                var dbSession = await access.Sessions.Where(s => s.Sid == sessionId).FirstOrDefaultAsync();
                if (dbSession == null) return ResultOr<AuthorizationSet>.Failed($"Could not get session: '{sessionId}'");
                if (!dbSession.Expired && dbSession.Expires - DateTime.Now <= TimeSpan.FromDays(1))
                {
                    DbSession extended = dbSession.Extend();
                    access.Sessions.Update(extended);
                    bool sessionExtended = await access.SaveChangesAsync() > 0;
                    if (sessionExtended)
                    {
                        dbSession = extended;
                        _log.Information("Extended session: '{SessionId}' to '{SessionExpirationDate}'", dbSession.Sid, dbSession.Expires);
                    }
                    else
                        _log.Warning("Failed to extend session: '{SessionId}'", dbSession.Sid);
                }
                var dbUser = await access.Users.Where(u => u.Uid == dbSession.Uid).FirstOrDefaultAsync();
                if (dbUser == null) return ResultOr<AuthorizationSet>.Failed($"Could not get user: '{dbSession.Uid}'");
                var dbGroup = await access.Groups.Where(g => g.Gid == dbUser.Gid).FirstOrDefaultAsync();
                if (dbGroup == null) return ResultOr<AuthorizationSet>.Failed($"Could not get group: '{dbUser.Gid}'");
                var set = new AuthorizationSet()
                {
                    Session = dbSession,
                    User = dbUser,
                    Group = dbGroup
                };
                return ResultOr<AuthorizationSet>.Ok(set);
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return ResultOr<AuthorizationSet>.Failed(e.Message, ResultStatus.InternalFail);
            }
        }

        public async Task<ResultOr<User>> InsertUserAsync(InsertUser user)
        {
            User? retUser;
            try
            {
                var dbNew = Utilities.ConvertFrom<DbUser, InsertUser>(user);
                if (dbNew == null) return ResultOr<User>.Failed("Failed to map user!", ResultStatus.InternalFail);
                
                DbAccess access = new DbAccess(_configuration);
                
                if (dbNew.Uid.IsNullEmptyWhiteSpace())
                {
                    if (user.Password.IsNullEmptyWhiteSpace()) return ResultOr<User>.Failed("To create a new user, create a password!");
                    dbNew.Uid = Guid.NewGuid().ToString();
                    retUser = Utilities.ConvertFrom<User, DbUser>(dbNew);
                    access.Users.Add(dbNew);
                }
                else
                {
                    var dbUser = await access.Users.Where(u => u.Uid == user.Uid).FirstOrDefaultAsync();
                    if (dbUser == null)
                        return ResultOr<User>.Failed($"Failed to get user by id: '{user.Uid}'");
                    dbUser.Update(user);
                    retUser = Utilities.ConvertFrom<User, DbUser>(dbUser);
                    access.Users.Update(dbUser);
                }
                bool saved = await access.SaveChangesAsync() > 0;
                if (!saved)
                {
                    _log.Warning("Failed to save user: {UserName} to database", user.Uid);
                    return ResultOr<User>.Failed("Could not save user to database!");
                }
            }
            catch (Exception e)
            {
                _log.Error(e, "Failed to insert user: {UserName}", user.Uid);
                return ResultOr<User>.Failed($"Failed to insert user: {user.Uid}", ResultStatus.InternalFail);
            }
            return retUser == null ? ResultOr<User>.Failed("Could not return user!") : ResultOr<User>.Ok(retUser);
        }
        
        public async Task<Result> RemoveUserAsync(string userId)
        {
            if (userId.IsNullEmptyWhiteSpace())
                return new Result("User id is empty!", ResultStatus.Failed);
            try
            {
                DbAccess access = new DbAccess(_configuration);
                DbUser? dbUser = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync();
                if (dbUser == null)
                    return new Result($"Could not find user: '{userId}'", ResultStatus.Failed);
                access.Users.Remove(dbUser);
                bool removed = await access.SaveChangesAsync() > 0;
                return new Result(removed ? "" : $"Failed to remove user: '{dbUser.Uid}'!", removed ? ResultStatus.Ok : ResultStatus.Failed);
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return new Result(e.Message, ResultStatus.InternalFail);
            }
        }

        public async Task<ListResult<UserItem>> GetUsersAsync(int results = 20, int skip = 0, string searchQuery = "")
        {
            if (results is 0 or > 20) results = 20;
            try
            {
                int totalCount;
                IEnumerable<DbUser> items;
                DbAccess access = new DbAccess(_configuration);
                if (!searchQuery.IsNullEmptyWhiteSpace())
                {
                    IQueryable<DbUser> queryResult = access.Users.Where(u =>
                        EF.Functions.Like(u.Uid, $"%{searchQuery}%") ||
                        EF.Functions.Like(u.Email, $"%{searchQuery}%") ||
                        EF.Functions.Like(u.DisplayName, $"%{searchQuery}%"));
                    items = await queryResult.Skip(skip).Take(results).ToListAsync();
                    totalCount = queryResult.Count();
                }
                else
                {
                    items = await access.Users.Skip(skip).Take(results).ToListAsync();
                    totalCount = access.Users.Count();
                }
                return new ListResult<UserItem>(items.Select(du => Utilities.ConvertFrom<UserItem, DbUser>(du) ?? new UserItem() { Uid = "NULL" }).Where(ui => ui.Uid != "NULL"), totalCount);
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return new ListResult<UserItem>(null, 0, e.Message, ResultStatus.InternalFail);
            }
        }

        public async Task<ResultOr<User>> GetUserAsync(string userId)
        {
            if (userId.IsNullEmptyWhiteSpace()) return new ResultOr<User>(null, "User ID is empty!", ResultStatus.Failed);
            try
            {
                DbAccess access = new DbAccess(_configuration);
                var dbUser = await access.Users.Where(u => u.Uid == userId).FirstOrDefaultAsync();
                var user = dbUser?.ToDto();
                return new ResultOr<User>(user, user == null ? $"Could not find user with id: {userId}!" : "", user == null ? ResultStatus.Failed : ResultStatus.Ok);
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return new ResultOr<User>(null, e.Message, ResultStatus.InternalFail);
            }
        }

        public async Task<ResultOr<Group>> InsertGroupAsync(InsertGroup group)
        {
            if (group.DisplayName.IsNullEmptyWhiteSpace()) return ResultOr<Group>.Failed("Display name is empty!");
            try
            {
                var dbNew = Utilities.ConvertFrom<DbGroup, InsertGroup>(group);
                if (dbNew == null) return ResultOr<Group>.Failed("Failed to map group!");
                
                DbAccess access = new DbAccess(_configuration);
                if (dbNew.Gid.IsNullEmptyWhiteSpace())
                {
                    dbNew.Gid = Guid.NewGuid().ToString();
                    access.Groups.Add(dbNew);
                }
                else
                {
                    var existingGroup = await access.Groups.Where(g => g.Gid == group.Gid).FirstOrDefaultAsync();
                    if (existingGroup == null)
                        return ResultOr<Group>.Failed($"Could not find a group with id: '{group.Gid}'");
                    access.Groups.Update(dbNew);
                }
                bool saved = await access.SaveChangesAsync() > 0;
                return saved ? ResultOr<Group>.Ok(Utilities.ConvertFrom<Group, DbGroup>(dbNew)) : ResultOr<Group>.Failed($"Failed to save group: '{@group.DisplayName}'");
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return ResultOr<Group>.Failed(e.Message, ResultStatus.InternalFail);
            }
        }

        public async Task<Result> RemoveGroup(string groupId)
        {
            if (groupId.IsNullEmptyWhiteSpace()) return new Result($"'{nameof(groupId)} is empty!'", ResultStatus.Failed);
            try
            {
                DbAccess access = new DbAccess(_configuration);
                DbGroup? dbGroup = await access.Groups.Where(g => g.Gid == groupId).FirstOrDefaultAsync();
                if (dbGroup == null)
                    return new Result($"Group with id: '{groupId}' does not exists!", ResultStatus.Failed);
                access.Groups.Remove(dbGroup);
                bool removed = await access.SaveChangesAsync() > 0;
                if (removed)
                    return new Result();
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return new Result(e.Message, ResultStatus.InternalFail);
            }

            return new Result($"Failed to remove group: '{groupId}'", ResultStatus.Failed);
        }

        public async Task<ListResult<GroupItem>> GetGroupsAsync(int results = 20, int skip = 0, string searchQuery = "")
        {
            try
            {
                int totalCount;
                IEnumerable<DbGroup> items;
                DbAccess access = new DbAccess(_configuration);
                if (!searchQuery.IsNullEmptyWhiteSpace())
                {
                    IQueryable<DbGroup> queryResult = access.Groups.Where(u =>
                        EF.Functions.Like(u.Gid, $"%{searchQuery}%") ||
                        EF.Functions.Like(u.DateCreated, $"%{searchQuery}%") ||
                        EF.Functions.Like(u.DisplayName, $"%{searchQuery}%"));
                    items = await queryResult.Skip(skip).Take(results).ToListAsync();
                    totalCount = queryResult.Count();
                }
                else
                {
                    items = await access.Groups.Skip(skip).Take(results).ToListAsync();
                    totalCount = access.Groups.Count();
                }
                return new ListResult<GroupItem>(items.Select(dg => Utilities.ConvertFrom<GroupItem, DbGroup>(dg) ?? new GroupItem() { Gid = "NULL" }).Where(gi => gi.Gid != "NULL"), totalCount);
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return new ListResult<GroupItem>(null, 0, e.Message, ResultStatus.InternalFail);
            }
        }

        public async Task<ResultOr<Group>> GetGroupAsync(string groupId)
        {
            if (groupId.IsNullEmptyWhiteSpace()) return ResultOr<Group>.Failed($"Parameter '{nameof(groupId)}' is empty!");
            try
            {
                DbAccess access = new DbAccess(_configuration);
                var group = Utilities.ConvertFrom<Group, DbGroup>(await access.Groups.Where(g => g.Gid == groupId).FirstOrDefaultAsync());
                return group == null ? ResultOr<Group>.Failed($"Failed to get group from id: {groupId}") : ResultOr<Group>.Ok(group);
            }
            catch (Exception e)
            {
                _log.Error(e, e.Message);
                return ResultOr<Group>.Failed(e.Message, ResultStatus.InternalFail);
            }
        }
    }
}