using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using IWS5.Core.Constants;
using IWS5.Core.DTO;
using IWS5.Core.Enums;
using IWS5.Core.Models;
using IWS5.Core.Models.Configuration;
using IWS5.Core.Models.Request;
using IWS5.Core.Services;
using IWS5.DatabaseAccess.Interfaces;
using IWS5.LdapClient.Interfaces;
using IWS5.LdapClient.Models;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Serilog;

namespace IWS5.Services;

public class AuthenticationService(
    ILdapClient ldapClient,
    IOptions<JwtOptions> jwtOptions,
    ILogger logger,
    IUserRepository userRepository,
    IUserPropertyRepository userPropertyRepository)
    : IAuthenticationService
{
    public async Task<CodeResponse<LogInResponseDto>> Login(LogInCredentials logInCredentials)
    {
        logger.Information("Login attempt started for user {UserName} in domain {Domain}", logInCredentials.UserName,
            logInCredentials.Domain);

        var ldapEntity = ldapClient.LdapLogIn(new LdapLogInRequest(
            logInCredentials.UserName,
            logInCredentials.Password,
            logInCredentials.Domain));

        if (ldapEntity == null)
        {
            logger.Information("LDAP authentication failed for user {UserName} in domain {Domain}", logInCredentials.UserName,
                logInCredentials.Domain);
            return new CodeResponse<LogInResponseDto>(ResponseCode.Unauthorized, "Not authorized");
        }

        var ldapGroups = ldapEntity.MemberOf
            .Where(g => g.OrganizationalUnits != null &&
                        g.OrganizationalUnits.Any(ou => ou.Equals("IntellectusPD", StringComparison.OrdinalIgnoreCase)))
            .SelectMany(group => group.CommonNames ?? Array.Empty<string>())
            .Where(n => !string.IsNullOrWhiteSpace(n))
            .Select(n => n.Trim())
            .Distinct(StringComparer.OrdinalIgnoreCase)
            .ToArray();

        var accessToken = GenerateJwtToken(ldapEntity.SAMAccountName, ldapGroups, ldapEntity.ObjectSid);

        var refreshToken = GenerateRefreshToken();
        var refreshTokenExpiry = DateTime.UtcNow.AddHours(jwtOptions.Value.RefreshTokenLifeTimeInHours);
        await userPropertyRepository.AddOrUpdateRefreshTokenData(ldapEntity.SAMAccountName, refreshToken, refreshTokenExpiry);

        logger.Information("Token generated successfully for user {UserName}", ldapEntity.SAMAccountName);
        var logInResponse = new LogInResponseDto(ldapEntity.SAMAccountName, accessToken, refreshToken);

        return new CodeResponse<LogInResponseDto>(ResponseCode.OK, null, logInResponse);
    }

    public async Task<CodeResponse<LogInResponseDto>> RefreshTokenAsync(RefreshTokenRequest refreshTokenRequest)
    {
        var normalizedUserName = refreshTokenRequest.UserName.Trim().ToLowerInvariant();

        try
        {
            var refreshTokenProperties = await userPropertyRepository
                .ListAsync(property => property.UserName == normalizedUserName
                                       && (property.PropertyName == AuthenticationConstants.RefreshTokenPropertyName ||
                                           property.PropertyName ==
                                           AuthenticationConstants.RefreshTokenExpiryPropertyName));

            var refreshTokenProperty = refreshTokenProperties
                .FirstOrDefault(p => p.PropertyName == AuthenticationConstants.RefreshTokenPropertyName);
            var refreshTokenExpiryProperty = refreshTokenProperties
                .FirstOrDefault(p => p.PropertyName == AuthenticationConstants.RefreshTokenExpiryPropertyName);

            if (refreshTokenProperty == null || refreshTokenExpiryProperty == null)
            {
                logger.Warning("Refresh token attempt for user with missing token data: {UserName}", normalizedUserName);
                return new CodeResponse<LogInResponseDto>(ResponseCode.Unauthorized, "Invalid or expired refresh token");
            }

            if (!DateTime.TryParse(refreshTokenExpiryProperty.PropertyValue, out var tokenExpiry))
            {
                logger.Error("Invalid refresh token expiry format for user: {UserName}", normalizedUserName);
                await userPropertyRepository.RemoveRefreshTokenData(normalizedUserName);
                return new CodeResponse<LogInResponseDto>(ResponseCode.Unauthorized,
                    "Invalid or expired refresh token");
            }

            var currentTime = DateTime.UtcNow;
            if (tokenExpiry <= currentTime.AddMinutes(-1))
            {
                logger.Information("Expired refresh token used for user: {UserName}", normalizedUserName);
                await userPropertyRepository.RemoveRefreshTokenData(normalizedUserName); // Cleanup expired token
                return new CodeResponse<LogInResponseDto>(ResponseCode.Unauthorized,
                    "Invalid or expired refresh token");
            }

            var storedToken = refreshTokenProperty.PropertyValue ?? string.Empty;
            var providedToken = refreshTokenRequest.RefreshToken;

            if (!ConstantTimeEquals(storedToken, providedToken))
            {
                logger.Warning("Invalid refresh token provided for user: {UserName}", normalizedUserName);
                return new CodeResponse<LogInResponseDto>(ResponseCode.Unauthorized,
                    "Invalid or expired refresh token");
            }

            var user = await userRepository.GetUserWithGroupsAsync(normalizedUserName);

            if (user == null)
            {
                logger.Error("User not found during refresh token validation: {UserName}", normalizedUserName);
                await userPropertyRepository.RemoveRefreshTokenData(normalizedUserName);
                return new CodeResponse<LogInResponseDto>(ResponseCode.Unauthorized,
                    "Invalid or expired refresh token");
            }
            
            var dbGroups = user.Parents
                .Select(parent => parent.UserName)
                .Where(n => !string.IsNullOrWhiteSpace(n))
                .Select(n => n.Trim())
                .ToArray();

            var newAccessToken = GenerateJwtToken(user.UserName, dbGroups, null);
            var newRefreshToken = GenerateRefreshToken();
            var newRefreshTokenExpiry = DateTime.UtcNow.AddHours(jwtOptions.Value.RefreshTokenLifeTimeInHours);

            var updateResult = await userPropertyRepository.AddOrUpdateRefreshTokenData(
                normalizedUserName,
                newRefreshToken,
                newRefreshTokenExpiry);

            if (!updateResult)
            {
                logger.Error("Failed to update refresh token data for user: {UserName}", normalizedUserName);
                return new CodeResponse<LogInResponseDto>(ResponseCode.InternalServerError, "Token refresh failed");
            }

            logger.Information("Refresh token successfully renewed for user: {UserName}", normalizedUserName);

            var logInResponse = new LogInResponseDto(user.UserName, newAccessToken, newRefreshToken);
            return new CodeResponse<LogInResponseDto>(ResponseCode.OK, null, logInResponse);
        }
        catch (Exception ex)
        {
            logger.Error(ex, "Unexpected error during refresh token validation for user: {UserName}",
                normalizedUserName);
            return new CodeResponse<LogInResponseDto>(ResponseCode.InternalServerError, "Token refresh failed");
        }
    }

    public async Task<CodeResponse<LogOutResponseDto>> LogOut(LogOutRequest logOutRequest)
    {
        var result = await userPropertyRepository.RemoveRefreshTokenData(logOutRequest.UserName);
        return new CodeResponse<LogOutResponseDto>(ResponseCode.OK, null, new LogOutResponseDto { LogOutSuccessful = result });
    }

    //crate some kind of salt to improve security
    private string GenerateJwtToken(string userName, string[] groups, string? userId)
    {
        var groupRoles = groups.Select(group => new Claim(ClaimTypes.Role, group)).ToArray();
        var claims = new List<Claim>
        {
            new(JwtRegisteredClaimNames.Email, userName),
            new(JwtRegisteredClaimNames.Sub, string.IsNullOrEmpty(userId) ? userName : userId)
        };
        claims.AddRange(groupRoles);

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Value.SecretKey));
        var signingCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var token = new JwtSecurityToken(
            jwtOptions.Value.Issuer,
            jwtOptions.Value.Audience,
            claims,
            null,
            DateTime.UtcNow.AddMinutes(jwtOptions.Value.TokenLifeTimeInMinutes),
            signingCredentials);

        var tokenValue = new JwtSecurityTokenHandler().WriteToken(token);
        return tokenValue;
    }

    private static string GenerateRefreshToken()
    {
        var randomNumber = new byte[32];
        using var rng = RandomNumberGenerator.Create();
        rng.GetBytes(randomNumber);
        return Convert.ToBase64String(randomNumber);
    }


    private static bool ConstantTimeEquals(string a, string b)
    {
        if (a == null || b == null)
            return a == b;

        if (a.Length != b.Length)
            return false;

        var result = 0;
        for (var i = 0; i < a.Length; i++)
        {
            result |= a[i] ^ b[i];
        }

        return result == 0;
    }
}
