using System.Diagnostics;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Authentication.Interfaces;
using adas_core.Authentication.Models;
using adas_core.Domain.Enums;
using adas_core.Domain.Exceptions;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.DTO;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using MongoDB.Bson;
using MongoDB.Driver;
using static System.Enum;

namespace adas_core.Authentication;

public class UserService : IUserService
{
    private readonly ILocalAuditService _auditService;
    private readonly IAuthorityService _authorityService;
    private readonly IEnumerable<ILoginService> _availableLoginServices;
    private readonly Lazy<IDisplayService> _displayService;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly JwtConfig _jwtConfig;
    private readonly ILogger<UserService> _logger;
    private readonly IUserRepository _userRepository;
    private readonly UsersWhiteListConfig _usersWhiteList;
    private readonly ValidGroupsConfig _validGroups;
    private readonly List<string> _validLoginMethods;
    private readonly ISubscribersService _subscribersService;
    private readonly IClientMessageService _clientMessageService;
    private readonly Lazy<IPermissionService> _permissionService;
    public UserService(
        IEnumerable<ILoginService> loginServices,
        IOptions<AuthSettings> configuration,
        IOptions<ValidGroupsConfig> validGroups,
        IOptions<AuthSettings> usersWhiteList,
        IOptions<AuthSettings> jwt,
        IHttpContextAccessor httpContextAccessor,
        IUserRepository userRepository,
        IAuthorityService authorityService,
        ILocalAuditService auditService,
        Lazy<IDisplayService> displayService,
        ILogger<UserService> logger,
        ISubscribersService subscribersService, 
        IClientMessageService clientMessageService, Lazy<IPermissionService> permissionService)
    {
        _availableLoginServices = loginServices;
        _httpContextAccessor = httpContextAccessor;
        _validGroups = validGroups.Value;
        _usersWhiteList = usersWhiteList.Value.UsersWhiteListConfig;
        _jwtConfig = jwt.Value.JwtConfig ?? throw new Exception("JWT NOT CONFIGURED");
        var loginMethods = configuration.Value.LoginMethods;
        _validLoginMethods = loginMethods;
        _userRepository = userRepository;
        _authorityService = authorityService;
        _auditService = auditService;
        _displayService = displayService;
        _logger = logger;
        _subscribersService = subscribersService;
        _clientMessageService = clientMessageService;
        _permissionService = permissionService;
    }


    public async Task<TokenResult> Login(string username, string password)
    {
        var user = await GetUser(username, password);
        _logger.LogInformation("[UserService] Login user {u}", user.ToString());

        if (user == null) throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedInvalidCredentials);
        if (!user.IsEnabled || (user.LockExpirationDate != null && user.LockExpirationDate > DateTime.UtcNow))
            throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenUserBlocked);
        if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user))
            throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission);

        if (user.LockExpirationDate < DateTime.UtcNow) user.LockExpirationDate = null;
        user.LastLogin = DateTime.UtcNow;
        await UpdateUsersByRequest(user, false);
        return await GenerateJwt(user);
    }

    public void ValidateToken(string token, string tokenType, out SecurityToken validatedToken)
    {
        try
        {
            var tokenValidationParameters = new TokenValidationParameters
            {
                ValidateAudience = _jwtConfig.ValidateAudience,
                ValidateIssuer = _jwtConfig.ValidateIssuer,
                ValidAudience = _jwtConfig.Audience,
                ValidIssuer = _jwtConfig.Issuer,
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Key ?? string.Empty)),
                ValidateLifetime = false
            };

            var tokenHandler = new JwtSecurityTokenHandler();
            tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
            if (securityToken is not JwtSecurityToken jwtSecurityToken ||
                !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256,
                    StringComparison.InvariantCultureIgnoreCase))
                throw new SecurityTokenException("Invalid token");
            validatedToken = securityToken;
        }
        catch (SecurityTokenException a)
        {
            _logger.LogError("Error validating token, {msg}", a.Message);
            throw new TokenException(a.Message);
        }
        catch (Exception e)
        {
            _logger.LogError("Error validating token, {msg}", e.Message);
            throw new TokenException("Error validating token");
        }
    }

    public async Task<User?> GetUserByToken(JwtSecurityToken? jwtToken)
    {
        User? user = null;
        if (jwtToken == null)
            return user;

        var userName = jwtToken.Claims.First(x => x.Type == ClaimTypes.Name).Value;

        user = await _userRepository.GetByUserName(userName);
        if (user == null)
            return user;

        //Eliminamos la contraseña
        user.Password = string.Empty;
        if (user.Authorization == null || user.Authorization.Count == 0)
            user.Authorization = await _authorityService.GetUserAuthorities(user.Id);
        return user;
    }

    public async Task<User?> GetUserByCasTicket(string serviceUrl, string ticket)
    {
        var service = _availableLoginServices.FirstOrDefault(s => s.Method == UserEnum.LoginMethod.Cas);
        if (service == null) throw new LoginServicesException("CAS login method not available");

        try
        {
            var user = await service.Login(_httpContextAccessor.HttpContext!);
            if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user))
                throw new UnauthorizedAccessException("You don't have sufficient permissions");
            return user;
        }
        catch (BusinessException)
        {
            throw;
        }
        catch (UnauthorizedAccessException)
        {
            throw;
        }
        catch (Exception e)
        {
            var message = e.Message;
            if (e.InnerException != null && e.Message != e.InnerException?.Message)
                message += $", {e.InnerException?.Message}";
            Debug.WriteLine($"Error while login: {message} ");
            throw new LoginServicesException(message, e);
        }
        ////////////////////////
        /*
        if (!_validLoginMethods.Contains(LoginMethod.CAS.ToString()))
        {
            Log.Warning("CAS login method not enabled");
            return null;
        }
        _casLoginService = new
        Log.Information("CAS ticket received: '{CasTicket}'", ticket);
        return string.IsNullOrEmpty(ticket) ? null : _casLoginService.GetUser(serviceUrl, ticket, context);
        */
    }

    public async Task<TokenResult> RefreshToken(string refreshToken)
    {
        ValidateToken(refreshToken, IUserService.TokenTypeRefresh, out var validatedToken);
        var user = await GetUserByToken((JwtSecurityToken)validatedToken);
        if (user == null)
            return new TokenResult();
        return await GenerateJwt(user);
    }


    public async Task<User> GetUser(string username, string password)
    {
        //BusinessException? loginException = null;
        foreach (var m in _validLoginMethods)
        {
            if (!TryParse(m, out UserEnum.LoginMethod method)) continue;
            var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword);
            if (service == null) continue;
            try
            {
                _logger.LogInformation("[UserService] Trying login with method {method}", method);
                return await service.Login(username, password);
            }
            catch (Exception e)
            {
                _logger.LogError("[UserService] Error while trying method {method}, err {err}", method, e.Message);
            }
        }

        throw new LoginServicesNotFoundException();
    }

    public async Task<User?> GetUserById(ObjectId id)
    {
        try
        {
            var u = await _userRepository.GetById(id);
            if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id);
            return u;
        }
        catch (Exception)
        {
            return null;
        }
    }

    public async Task<User?> GetUserByUserName(string name)
    {
        var u = await _userRepository.GetByUserName(name);
        if (u == null) return u;
        u.Authorization = await _authorityService.GetUserAuthorities(u.Id);
        return u;
    }

    public async Task<User?> GetUserByName(string name)
    {
        try
        {
            var u = await _userRepository.GetByName(name);
            if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id);
            return u;
        }
        catch (Exception)
        {
            return null;
        }
    }

    public async Task<User?> CreateUser(User userEntryLdap)
    {
        CheckIfUserNameExists(userEntryLdap.UserName);
        CheckIfEmailExists(userEntryLdap.Email);
        await _userRepository.InsertOneAsync(userEntryLdap);
        var result = await GetUserByUserName(userEntryLdap.UserName);
        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result);
        return result;
    }

    public async Task<User?> CreateNewUserByRequest(User user)
    {
        CheckIfEmailExists(user.Email);
        CheckIfUserNameExists(user.UserName);
        if (user.Type == UserEnum.Type.Local)
        {
            if (!CryptoAdas.IsStrongPassword(user.Password))
                throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword);
            user.Password = CryptoAdas.CreateBCrypt(user.Password);
        }

        await _userRepository.InsertOneAsync(user);
        var result = await GetUserByUserName(user.UserName) ??
                     throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result);
        return result;
    }

    public async Task<User?> CreateNewUserWithAuthorities(CreateUserWithAuthDto createUserWithAuthDto)
    {
        var user = createUserWithAuthDto.User;
        var newUser = await CreateNewUserByRequest(user) ??
                      throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        var auths = createUserWithAuthDto.Authorizations;
        foreach (var auth in auths)
        {
            auth.UserId = newUser.Id;
            await CreateNewAuthority(auth);
        }

        return newUser;
    }

    public async Task<User?> UpdateUsersByRequest(User user, bool updatePass)
    {
        var oldUser = await _userRepository.GetByUserName(user.UserName);
        var logoutHasBeenSent = false;
        if (updatePass && !CryptoAdas.IsStrongPassword(user.Password))
            throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword);
        if (updatePass)
        {
            user.Password = CryptoAdas.CreateBCrypt(user.Password);
            await SendBroadcastUser(user, user.UserName, OperationType.UpdatePassword);
            logoutHasBeenSent = true;
        }
        
        var result = await _userRepository.UpdateUser(user, updatePass);
        if(!logoutHasBeenSent && oldUser != null && result != null && oldUser.IsEnabled && !result.IsEnabled)
        {
            await SendBroadcastUser(result, user.UserName, OperationType.DisableUser);
            logoutHasBeenSent = true;
        }
        if(!logoutHasBeenSent && result != null && result.LockExpirationDate != null && result.LockExpirationDate>DateTime.UtcNow) await SendBroadcastUser(result, user.UserName, OperationType.LockUser);
        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, oldUser, result);
        return result;
    }

    private async Task SendBroadcastUser(object elementUpdated, string userName, OperationType operation)
    {
        var subscribers = _subscribersService.GetSubscribers().Where(s =>
            s.UserName == userName).ToList();
        foreach (var subscriber in subscribers)
        {
            await _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated);
        }
    }
    private async Task SendBroadcastPermissions(object elementUpdated, string userName, OperationType operation)
    {
        var subscribers = _subscribersService.GetSubscribers().Where(s =>
            s.UserName == userName).ToList();
        foreach (var subscriber in subscribers)
        {
            if (subscriber.SubscriptionType == SubscriptionEnum.WsType.Admin)
            {
                try
                {
                    var permissionsForPanel = await _permissionService.Value.GetPermissionsForPanel(userName);
                    _ = _clientMessageService.SendAsync(subscriber.Id, operation, permissionsForPanel);
                }
                catch (Exception)
                {
                    _ = _clientMessageService.SendAsync(subscriber.Id, operation, null);
                }
                
            }
            else
            {
                _ = _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated);
            }
        }
    }
    public async Task<User?> UpdateUserWithAuthorities(UpdateUserWithAuthDto updateUserWithAuthDto, bool updatePass)
    {
        var u = updateUserWithAuthDto.User;
        CheckIfEmailExists(u.Email, u.Id);
        CheckIfUserNameExists(u.UserName, u.Id);
        var userUpdated = await UpdateUsersByRequest(u, updatePass) ??
                          throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);

        foreach (var id in updateUserWithAuthDto.AuthsToDelete) await DeleteAuthority(id);
        foreach (var auth in updateUserWithAuthDto.Authorizations) await UpdateAuthority(auth);
        foreach (var auth in updateUserWithAuthDto.NewAuthorizations)
        {
            auth.UserId = userUpdated.Id;
            await CreateNewAuthority(auth);
        }
        if(updateUserWithAuthDto.AuthsToDelete.Any() || updateUserWithAuthDto.NewAuthorizations.Any() ||  updateUserWithAuthDto.Authorizations.Any())
        {
            var allPermissionsForDisplay = await _displayService.Value.GetAllByUser(u.UserName);

            await SendBroadcastPermissions(allPermissionsForDisplay, u.UserName, OperationType.UpdateAuthorities);
        }
        return userUpdated;
    }

    public async Task<bool> UpdateUserPassword(ObjectId id, string oldPassword, string newPassword)
    {
        if (oldPassword == newPassword)
            throw new BadRequestException("New password must be different");

        var user = await _userRepository.GetById(id) ??
                   throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);


        if (!CryptoAdas.VerifyPassword(oldPassword, user.Password))
            throw new UnprocessableEntityException("Invalid current password");

        if (!CryptoAdas.IsStrongPassword(newPassword))
            throw new UnprocessableEntityException("Weak password");
                
        user.Password = CryptoAdas.CreateBCrypt(newPassword);
        var result = await _userRepository.UpdateUser(user, true);
        if (result != null)
        {
            _logger.LogInformation("Password updated for user {UserId}", id);

            await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, result);
            return true;
        }
       
        

        return false;
    }

    public async Task<bool> DeleteUser(ObjectId id)
    {
        try
        {
            var user = await _userRepository.GetById(id);
            await _userRepository.DeleteAsync(id);
            await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, null);
            return true;
        }
        catch (Exception e)
        {
            _logger.LogError("Error deleting user. Exception: {e}", e);
            return false;
        }
    }

    public async Task<PaginationResponse<User>> GetPaginatedUsers(PaginationFilter filter)
    {
        var result = _userRepository.GetPaginatedUsers(filter);

        var count = await result.CountDocumentsAsync();

        var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
            .Limit(filter.PageSize)
            .ToCursorAsync();


        var dataList = await data.ToListAsync();

        return new PaginationResponse<User>(dataList, filter.PageNumber, filter.PageSize, count);
    }

    public async Task<TokenResult> GenerateJwt(User user)
    {
        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, user.UserName),
            new(ClaimTypes.Email, user.Email),
            new("IpAddress", user.IpAddress)
        };

        // Agregar claims de autorizaciones utilizando ClaimsFromAuthorities
        if (user.Authorization != null)
            claims.AddRange(await ClaimsFromAuthorities(user.Authorization));

        var accessToken = JwtHelper.GetJwtToken(
            user.UserName,
            _jwtConfig.Key ?? string.Empty,
            _jwtConfig.Issuer ?? string.Empty,
            _jwtConfig.Audience ?? string.Empty,
            _jwtConfig.TokenValidityInMinutes,
            claims.ToArray());

        var refreshToken = JwtHelper.GetJwtToken(
            user.UserName,
            _jwtConfig.Key ?? string.Empty,
            _jwtConfig.Issuer ?? string.Empty,
            _jwtConfig.Audience ?? string.Empty,
            _jwtConfig.RefreshTokenValidityInMinutes,
            claims.ToArray());

        return new TokenResult
        {
            AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken),
            RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken),
            AccessTokenExpiryTime = accessToken.ValidTo,
            RefreshTokenExpiryTime = refreshToken.ValidTo,
            User = user
        };
    }

    public async Task<List<User>> GetAll()
    {
        BusinessException? loginException = null;
        foreach (var m in _validLoginMethods)
        {
            if (!TryParse(m, out UserEnum.LoginMethod method)) continue;
            var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword);
            if (service == null) continue;
            try
            {
                return await service.GetAllUsers();
            }
            catch (BusinessException e)
            {
                loginException = e;
            }
            catch (Exception e)
            {
                var message = e.Message;
                if (e.Message != e.InnerException?.Message) message += $", {e.InnerException?.Message}";
                Debug.WriteLine($"Error Getting all users: {message} ");
                loginException = new LoginServicesException(message);
            }
        }


        if (loginException != null) throw loginException;

        throw new LoginServicesNotFoundException();
    }

    public async Task<TokenResult> LoginWithGivenAccessToken(string token)
    {
        ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken);
        var jwtToken = (JwtSecurityToken)validatedToken;
        var user = await GetUserByToken(jwtToken) ??
                   throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id);

        if (jwtToken.ValidTo < DateTime.UtcNow)
            return await GenerateJwt(user);


        var claims = new List<Claim>
        {
            new(ClaimTypes.Name, user.UserName),
            new(ClaimTypes.Email, user.Email),
            new("IpAddress", user.IpAddress)
        };

        // Agregar claims de autorizaciones utilizando ClaimsFromAuthorities
        claims.AddRange(await ClaimsFromAuthorities(user.Authorization));
        var dif = jwtToken.ValidTo > DateTime.Now ? jwtToken.ValidTo - DateTime.Now : DateTime.Now - jwtToken.ValidTo;
        var accessToken = JwtHelper.GetJwtToken(
            user.UserName,
            _jwtConfig.Key ?? string.Empty,
            _jwtConfig.Issuer ?? string.Empty,
            _jwtConfig.Audience ?? string.Empty,
            (int)dif.TotalMinutes,
            claims.ToArray());
        var refreshToken = JwtHelper.GetJwtToken(
            user.UserName,
            _jwtConfig.Key ?? string.Empty,
            _jwtConfig.Issuer ?? string.Empty,
            _jwtConfig.Audience ?? string.Empty,
            (int)dif.TotalMinutes + _jwtConfig.RefreshTokenValidityInMinutes,
            claims.ToArray());

        return new TokenResult
        {
            AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken),
            RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken),
            AccessTokenExpiryTime = accessToken.ValidTo,
            RefreshTokenExpiryTime = refreshToken.ValidTo,
            User = user
        };
    }

    public async Task<TokenResult> LoginWithAccessToken(string token)
    {
        ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken);
        var jwtToken = (JwtSecurityToken)validatedToken;
        var user = await GetUserByToken(jwtToken) ??
                   throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id);
        
        return await GenerateJwt(user);
    }
    
    public async Task<Authorization> CreateNewAuthority(Authorization auth)
    {
        var newUserAuthority = await _authorityService.CreateNewUserAuthority(auth) ??
                               throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
        return newUserAuthority;
    }

    public async Task<bool> DeleteAuthority(string id)
    {
        if (!ObjectId.TryParse(id, out var userAuthorityIdParsed))
            throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat);
        var isDeleted = await _authorityService.DeleteUserAuthority(userAuthorityIdParsed);

        return !isDeleted ? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed) : isDeleted;
    }

    public async Task<bool> UpdateAuthority(Authorization authorization)
    {
        _ = await _authorityService.EditUserAuthority(authorization) ??
            throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
        return true;
    }

    public async Task<List<Claim>> ClaimsFromAuthorities(List<Authorization> authorities)
    {
        var claims = new List<Claim>();
        foreach (var auth in authorities)
            if (auth.UnitId != null)
            {
                var dis = await
                    _displayService.Value.GetByUnitId(ObjectId.Parse(auth.UnitId));
                foreach (var claim in dis)
                {
                    auth.DisplayId = claim.Id.ToString();
                    AddClaim(auth, claims);
                }
            }
            else
            {
                AddClaim(auth, claims);
            }

        return claims;
    }

    private static void AddClaim(Authorization auth, List<Claim> claims)
    {
        var newClaim = new Claim(ClaimTypes.Role,
            auth.Rol + (string.IsNullOrEmpty(auth.DisplayId) ? "" : "_" + auth.DisplayId));
        if (claims.Any(c => c.Value == newClaim.Value)) return;

        //newClaim.Properties.Add("DisplayId", auth.DisplayId ?? "");
        claims.Add(newClaim);
    }

    private void CheckIfUserNameExists(string userName, ObjectId? userId = null)
    {
        var users = GetAll().Result;
        var userNameExists = userId.HasValue
            ? users.Any(user => user.Id != userId.Value && user.UserName == userName)
            : users.Any(user => user.UserName == userName);

        if (userNameExists) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername);
    }

    private void CheckIfEmailExists(string email, ObjectId? userId = null)
    {
        var users = GetAll().Result;
        var emailExists = userId.HasValue
            ? users.Any(user => user.Id != userId.Value && user.Email == email)
            : users.Any(user => user.Email == email);

        if (emailExists && !string.IsNullOrEmpty(email))
            throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername);
    }
}