using CalledIt.DataAccess;
using CalledIt.DataAccess.Entities;
using CalledIt.Domain.Dtos;
using Microsoft.EntityFrameworkCore;

namespace CalledIt.Domain.Services;

public class UserService : IUserService
{
    private const int MaxFailedLoginAttempts = 5;
    private static readonly TimeSpan _lockoutDuration = TimeSpan.FromMinutes(15);

    private readonly CalledItDbContext _dbContext;
    private readonly IPasswordService _passwordService;

    public UserService(CalledItDbContext dbContext, IPasswordService passwordService)
    {
        _dbContext = dbContext;
        _passwordService = passwordService;
    }

    public async Task<UserResponseDto> CreateAsync(CreateUserRequestDto userDto)
    {
        var usernameTaken = await _dbContext.Users
            .AnyAsync(u => u.Username == userDto.Username);

        if (usernameTaken)
        {
            throw new InvalidOperationException("Username is already taken.");
        }

        var emailTaken = await _dbContext.Users
            .AnyAsync(u => u.Email == userDto.Email);

        if (emailTaken)
        {
            throw new InvalidOperationException("Email is already in use.");
        }

        var entity = new UserEntity
        {
            Username = userDto.Username,
            Email = userDto.Email,
            PasswordHash = string.Empty,
            FailedLoginAttempts = 0,
            LockoutEndUtc = null
        };

        entity.PasswordHash = _passwordService.HashPassword(entity, userDto.Password);

        _dbContext.Users.Add(entity);

        try
        {
            await _dbContext.SaveChangesAsync();
        }
        catch (DbUpdateException)
        {
            // Lost a race against a concurrent registration — the optimistic
            // checks above passed, but the DB unique index on Username/Email
            // rejected the insert. Re-check to surface the right 409 message
            // instead of letting this bubble up as a 500.
            if (await _dbContext.Users.AnyAsync(u => u.Username == userDto.Username))
            {
                throw new InvalidOperationException("Username is already taken.");
            }

            if (await _dbContext.Users.AnyAsync(u => u.Email == userDto.Email))
            {
                throw new InvalidOperationException("Email is already in use.");
            }

            throw;
        }

        return new UserResponseDto(entity.Id, entity.Username, entity.Email);
    }

    public async Task<UserResponseDto> GetByIdAsync(int id)
    {
        var entity = await _dbContext.Users
            .AsNoTracking()
            .SingleOrDefaultAsync(user => user.Id == id);

        if (entity == null)
        {
            throw new KeyNotFoundException($"User with ID {id} not found.");
        }

        return new UserResponseDto(entity.Id, entity.Username, entity.Email);
    }

    public async Task<UserResponseDto> GetByUsernameAsync(string username)
    {
        var entity = await _dbContext.Users
            .AsNoTracking()
            .SingleOrDefaultAsync(user => user.Username == username);

        if (entity == null)
        {
            throw new KeyNotFoundException($"User with username '{username}' not found.");
        }

        return new UserResponseDto(entity.Id, entity.Username, entity.Email);
    }

    public async Task<AuthenticatedUserDto> LoginAsync(LoginRequestDto loginRequestDto)
    {
        var entity = await _dbContext.Users
            .SingleOrDefaultAsync(user => user.Username == loginRequestDto.Username);

        if (entity == null)
        {
            throw new UnauthorizedAccessException("Invalid username or password.");
        }

        if (entity.LockoutEndUtc is not null && entity.LockoutEndUtc > DateTime.UtcNow)
        {
            throw new UnauthorizedAccessException(
                $"Account locked. Try again after {entity.LockoutEndUtc:O}.");
        }

        var passwordMatches = _passwordService.VerifyHashedPassword(
            entity,
            entity.PasswordHash,
            loginRequestDto.Password);

        if (!passwordMatches)
        {
            entity.FailedLoginAttempts += 1;

            if (entity.FailedLoginAttempts >= MaxFailedLoginAttempts)
            {
                entity.LockoutEndUtc = DateTime.UtcNow.Add(_lockoutDuration);
                entity.FailedLoginAttempts = 0;
            }

            await _dbContext.SaveChangesAsync();

            throw new UnauthorizedAccessException("Invalid username or password.");
        }

        entity.FailedLoginAttempts = 0;
        entity.LockoutEndUtc = null;
        await _dbContext.SaveChangesAsync();

        return new AuthenticatedUserDto(entity.Id, entity.Username, entity.IsAdmin);
    }

    public async Task<AuthenticatedUserDto> GetAuthenticatedAsync(int id)
    {
        var entity = await _dbContext.Users
            .AsNoTracking()
            .SingleOrDefaultAsync(user => user.Id == id);

        if (entity == null)
        {
            throw new KeyNotFoundException($"User with ID {id} not found.");
        }

        return new AuthenticatedUserDto(entity.Id, entity.Username, entity.IsAdmin);
    }
}
