using OmsorgCore.Application.Abstractions;
using OmsorgCore.Application.Models;
using OmsorgCore.Domain.Entities;
using OmsorgCore.Domain.Enums;

namespace OmsorgCore.Application.Services;

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;
    private readonly IEmployeeRepository _employeeRepository;
    private readonly IRoleRepository _roleRepository;
    private readonly IPasswordHasher _passwordHasher;
    private readonly IPasswordResetService _passwordResetService;
    private readonly IRefreshTokenRepository _refreshTokenRepository;
    private readonly IPasswordPolicy _passwordPolicy;
    private readonly IPermissionService _permissionService;

    public UserService(
        IUserRepository userRepository,
        IEmployeeRepository employeeRepository,
        IRoleRepository roleRepository,
        IPasswordHasher passwordHasher,
        IPasswordResetService passwordResetService,
        IRefreshTokenRepository refreshTokenRepository,
        IPasswordPolicy passwordPolicy,
        IPermissionService permissionService)
    {
        _userRepository = userRepository;
        _employeeRepository = employeeRepository;
        _roleRepository = roleRepository;
        _passwordHasher = passwordHasher;
        _passwordResetService = passwordResetService;
        _refreshTokenRepository = refreshTokenRepository;
        _passwordPolicy = passwordPolicy;
        _permissionService = permissionService;
    }

    public async Task<IReadOnlyList<UserSummary>> GetAllAsync(CancellationToken cancellationToken = default)
    {
        var users = await _userRepository.GetAllAsync(cancellationToken);
        return users
            .Select(u => new UserSummary(u.Id, u.Username, u.EmployeeId, u.Role.Name, u.IsActive, u.MustChangePassword))
            .ToList();
    }

    public async Task<CreateUserResult> CreateForEmployeeAsync(
        Guid employeeId,
        string username,
        Guid roleId,
        UserCreationMode mode,
        string? initialPassword,
        TimeSpan? invitePinValidity,
        CancellationToken cancellationToken = default)
    {
        var employee = await _employeeRepository.GetByIdAsync(employeeId, cancellationToken);
        if (employee is null)
        {
            return CreateUserResult.Fail(CreateUserFailureReason.EmployeeNotFound);
        }

        var existingForEmployee = await _userRepository.GetByEmployeeIdAsync(employeeId, cancellationToken);
        if (existingForEmployee is not null)
        {
            return CreateUserResult.Fail(CreateUserFailureReason.EmployeeAlreadyHasAccount);
        }

        if (await _userRepository.ExistsByUsernameAsync(username, cancellationToken))
        {
            return CreateUserResult.Fail(CreateUserFailureReason.UsernameTaken);
        }

        var role = await _roleRepository.GetByIdAsync(roleId, cancellationToken);
        if (role is null)
        {
            return CreateUserResult.Fail(CreateUserFailureReason.RoleNotFound);
        }

        if (mode == UserCreationMode.Direct && string.IsNullOrWhiteSpace(initialPassword))
        {
            return CreateUserResult.Fail(CreateUserFailureReason.InitialPasswordRequired);
        }

        if (mode == UserCreationMode.Direct && !_passwordPolicy.IsValid(initialPassword))
        {
            return CreateUserResult.Fail(CreateUserFailureReason.PasswordTooShort);
        }

        if (mode == UserCreationMode.Invite && string.IsNullOrWhiteSpace(employee.Email))
        {
            return CreateUserResult.Fail(CreateUserFailureReason.EmployeeEmailMissing);
        }

        var user = new User
        {
            Username = username,
            RoleId = roleId,
            EmployeeId = employeeId,
            // Invite-Modus: kein für den Login nutzbares Passwort - Hash eines Zufallswerts, den niemand
            // je eingibt. Der Mitarbeiter setzt sein eigenes Passwort über den unten ausgestellten PIN-Flow.
            PasswordHash = mode == UserCreationMode.Direct
                ? _passwordHasher.Hash(initialPassword!)
                : _passwordHasher.Hash(Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N")),
            MustChangePassword = mode == UserCreationMode.Direct
        };

        await _userRepository.AddAsync(user, cancellationToken);
        await _userRepository.SaveChangesAsync(cancellationToken);

        if (mode == UserCreationMode.Invite)
        {
            var validity = invitePinValidity ?? TimeSpan.FromDays(7);
            var rawPin = await _passwordResetService.IssueInviteCodeAsync(user.Id, validity, cancellationToken);
            return CreateUserResult.Ok(user.Id, role.Name, employee.Email, rawPin);
        }

        return CreateUserResult.Ok(user.Id, role.Name);
    }

    public async Task<ChangeOwnPasswordResult> ChangeOwnPasswordAsync(
        Guid userId,
        string currentPassword,
        string newPassword,
        CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByIdAsync(userId, cancellationToken);
        if (user is null || !_passwordHasher.Verify(currentPassword, user.PasswordHash))
        {
            return ChangeOwnPasswordResult.InvalidCurrentPassword;
        }

        if (!_passwordPolicy.IsValid(newPassword))
        {
            return ChangeOwnPasswordResult.PasswordTooShort;
        }

        user.PasswordHash = _passwordHasher.Hash(newPassword);
        user.MustChangePassword = false;

        // Wie nach einem Passwort-Reset: bestehende Sessions beenden, SecurityStamp erneuern - der
        // gerade benutzte Access-Token wird dadurch ab dem nächsten Request ungültig, ein neuer Login
        // mit dem neuen Passwort ist danach nötig (siehe PasswordResetService.ResetPasswordAsync).
        var activeSessions = await _refreshTokenRepository.GetActiveByUserIdAsync(userId, cancellationToken);
        foreach (var session in activeSessions)
        {
            session.RevokedAt = DateTime.UtcNow;
        }
        user.SecurityStamp = Guid.NewGuid();

        await _userRepository.SaveChangesAsync(cancellationToken);
        await _refreshTokenRepository.SaveChangesAsync(cancellationToken);
        return ChangeOwnPasswordResult.Success;
    }

    public async Task<AdminResetPasswordResult> AdminResetPasswordAsync(
        Guid userId,
        UserCreationMode mode,
        string? initialPassword,
        TimeSpan? invitePinValidity,
        CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByIdWithEmployeeAsync(userId, cancellationToken);
        if (user is null)
        {
            return AdminResetPasswordResult.Fail(AdminResetPasswordFailureReason.UserNotFound);
        }

        if (mode == UserCreationMode.Direct && string.IsNullOrWhiteSpace(initialPassword))
        {
            return AdminResetPasswordResult.Fail(AdminResetPasswordFailureReason.InitialPasswordRequired);
        }

        if (mode == UserCreationMode.Direct && !_passwordPolicy.IsValid(initialPassword))
        {
            return AdminResetPasswordResult.Fail(AdminResetPasswordFailureReason.PasswordTooShort);
        }

        if (mode == UserCreationMode.Invite && string.IsNullOrWhiteSpace(user.Employee?.Email))
        {
            return AdminResetPasswordResult.Fail(AdminResetPasswordFailureReason.EmployeeEmailMissing);
        }

        if (mode == UserCreationMode.Direct)
        {
            user.PasswordHash = _passwordHasher.Hash(initialPassword!);
            user.MustChangePassword = true;

            // Admin-gesetztes Passwort ersetzt sofort alle laufenden Sessions - wie ein regulärer
            // Passwort-Reset (siehe PasswordResetService.ResetPasswordAsync).
            var activeSessions = await _refreshTokenRepository.GetActiveByUserIdAsync(userId, cancellationToken);
            foreach (var session in activeSessions)
            {
                session.RevokedAt = DateTime.UtcNow;
            }
            user.SecurityStamp = Guid.NewGuid();

            await _userRepository.SaveChangesAsync(cancellationToken);
            await _refreshTokenRepository.SaveChangesAsync(cancellationToken);
            return AdminResetPasswordResult.Ok();
        }

        var validity = invitePinValidity ?? TimeSpan.FromDays(7);
        var rawPin = await _passwordResetService.IssueInviteCodeAsync(user.Id, validity, cancellationToken);
        return AdminResetPasswordResult.Ok(user.Employee!.Email, rawPin);
    }

    public async Task<UpdateUserResult> UpdateAsync(
        Guid userId,
        Guid roleId,
        bool isActive,
        CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByIdAsync(userId, cancellationToken);
        if (user is null)
        {
            return UpdateUserResult.Fail(UpdateUserFailureReason.UserNotFound);
        }

        var role = await _roleRepository.GetByIdAsync(roleId, cancellationToken);
        if (role is null)
        {
            return UpdateUserResult.Fail(UpdateUserFailureReason.RoleNotFound);
        }

        user.RoleId = roleId;
        _permissionService.InvalidateUserPermissions(userId);

        var wasActive = user.IsActive;
        user.IsActive = isActive;

        if (wasActive && !isActive)
        {
            // Sofortiger Killswitch bei Deaktivierung - kein Warten auf Token-Ablauf (siehe SessionAdminService).
            var activeSessions = await _refreshTokenRepository.GetActiveByUserIdAsync(userId, cancellationToken);
            foreach (var session in activeSessions)
            {
                session.RevokedAt = DateTime.UtcNow;
            }
            user.SecurityStamp = Guid.NewGuid();
            await _refreshTokenRepository.SaveChangesAsync(cancellationToken);
        }

        await _userRepository.SaveChangesAsync(cancellationToken);
        return UpdateUserResult.Ok();
    }

    public async Task<IReadOnlyList<PermissionOverrideSummary>?> GetPermissionOverridesAsync(
        Guid userId, CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByIdWithPermissionsAsync(userId, cancellationToken);
        if (user is null)
        {
            return null;
        }

        return user.PermissionOverrides
            .Select(o => new PermissionOverrideSummary(o.Id, o.Module, o.Action, o.Effect, o.Scope))
            .ToList();
    }

    public async Task<AddPermissionOverrideResult> AddPermissionOverrideAsync(
        Guid userId,
        ModuleType module,
        PermissionAction action,
        PermissionEffect effect,
        PermissionScope scope,
        CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByIdWithPermissionsAsync(userId, cancellationToken);
        if (user is null)
        {
            return AddPermissionOverrideResult.Fail(AddPermissionOverrideFailureReason.UserNotFound);
        }

        var existing = user.PermissionOverrides
            .FirstOrDefault(o => o.Module == module && o.Action == action);

        if (existing is not null)
        {
            existing.Effect = effect;
            existing.Scope = scope;
        }
        else
        {
            existing = new UserPermissionOverride { UserId = user.Id, Module = module, Action = action, Effect = effect, Scope = scope };
            await _userRepository.AddPermissionOverrideAsync(existing, cancellationToken);
        }

        await _userRepository.SaveChangesAsync(cancellationToken);
        _permissionService.InvalidateUserPermissions(userId);
        return AddPermissionOverrideResult.Ok(new PermissionOverrideSummary(existing.Id, module, action, effect, scope));
    }

    public async Task<RemovePermissionOverrideResult> RemovePermissionOverrideAsync(
        Guid userId, Guid overrideId, CancellationToken cancellationToken = default)
    {
        var user = await _userRepository.GetByIdWithPermissionsAsync(userId, cancellationToken);
        if (user is null)
        {
            return RemovePermissionOverrideResult.Fail(RemovePermissionOverrideFailureReason.UserNotFound);
        }

        var existing = user.PermissionOverrides.FirstOrDefault(o => o.Id == overrideId);
        if (existing is null)
        {
            return RemovePermissionOverrideResult.Fail(RemovePermissionOverrideFailureReason.OverrideNotFound);
        }

        user.PermissionOverrides.Remove(existing);
        await _userRepository.SaveChangesAsync(cancellationToken);
        _permissionService.InvalidateUserPermissions(userId);
        return RemovePermissionOverrideResult.Ok();
    }
}
