using System.Net;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using ROLAC.API.Data;
using ROLAC.API.DTOs.Invitations;
using ROLAC.API.Entities;
using ROLAC.API.Entities.Logging;
using ROLAC.API.Services.Logging;
using ROLAC.API.Services.Notifications;

namespace ROLAC.API.Services;

public class InvitationService : IInvitationService
{
    /// <summary>Lifetime of a freshly issued invitation link.</summary>
    private const int InvitationLifetimeDays = 7;

    private readonly UserManager<AppUser> _userManager;
    private readonly AppDbContext         _db;
    private readonly ITokenService        _tokenService;
    private readonly IEmailService        _emailService;
    private readonly IAuditLogger         _audit;
    private readonly CurrentUserAccessor  _currentUser;

    public InvitationService(
        UserManager<AppUser> userManager,
        AppDbContext         db,
        ITokenService        tokenService,
        IEmailService        emailService,
        IAuditLogger         audit,
        CurrentUserAccessor  currentUser)
    {
        _userManager  = userManager;
        _db           = db;
        _tokenService = tokenService;
        _emailService = emailService;
        _audit        = audit;
        _currentUser  = currentUser;
    }

    // ── Create ───────────────────────────────────────────────────────────────

    public async Task<CreateInvitationResult> CreateAsync(CreateInvitationRequest request)
    {
        var member = await _db.Members.FindAsync(request.MemberId)
            ?? throw new InvalidOperationException($"Member {request.MemberId} does not exist.");

        var email = (request.Email ?? member.Email)?.Trim();
        if (string.IsNullOrWhiteSpace(email))
            throw new InvalidOperationException(
                "This member has no email address. Add an email before creating an invitation.");

        var user = await _userManager.Users.FirstOrDefaultAsync(u => u.MemberId == request.MemberId);
        if (user is null)
            user = await CreateAccountAsync(member, email, request.Roles);

        var now = DateTime.UtcNow;

        // Re-issue: revoke any prior unused invitation so only one link is ever live.
        var existing = await _db.UserInvitations
            .Where(invitation => invitation.UserId == user.Id
                              && invitation.UsedAt == null
                              && invitation.RevokedAt == null)
            .ToListAsync();
        foreach (var invitation in existing)
            invitation.RevokedAt = now;

        var rawToken  = GenerateRawToken();
        var expiresAt = now.AddDays(InvitationLifetimeDays);

        _db.UserInvitations.Add(new UserInvitation
        {
            UserId    = user.Id,
            TokenHash = _tokenService.HashToken(rawToken),
            ExpiresAt = expiresAt,
            CreatedAt = now,
            CreatedBy = _currentUser.UserIdOrSystem,
        });

        await _db.SaveChangesAsync();

        _audit.Write(
            AuditActions.InvitationCreated, AuditCategories.Security, LogLevelEnum.Information,
            entityName: nameof(AppUser), entityId: user.Id,
            summary: $"Invitation link created for {user.Email}");

        return new CreateInvitationResult { Token = rawToken, ExpiresAt = expiresAt };
    }

    /// <summary>Creates a passwordless login account linked to the member; mirrors UserManagementService.</summary>
    private async Task<AppUser> CreateAccountAsync(Member member, string email, List<string>? roles)
    {
        if (await _userManager.FindByEmailAsync(email) is not null)
            throw new InvalidOperationException($"Email '{email}' is already in use by another account.");

        var user = new AppUser
        {
            UserName           = email,
            Email              = email,
            EmailConfirmed     = true,
            MemberId           = member.Id,
            LanguagePreference = member.LanguagePreference,
            IsActive           = true,
            CreatedAt          = DateTime.UtcNow,
        };

        // No-password overload: the member sets their own password via the invitation link.
        var result = await _userManager.CreateAsync(user);
        if (!result.Succeeded)
            throw new InvalidOperationException(
                string.Join("; ", result.Errors.Select(error => error.Description)));

        var rolesToAssign = roles is { Count: > 0 } ? roles : new List<string> { "member" };
        await _userManager.AddToRolesAsync(user, rolesToAssign);

        return user;
    }

    // ── Validate ───────────────────────────────────────────────────────────────

    public async Task<ValidateInvitationResult> ValidateAsync(string rawToken)
    {
        var invitation = await FindByRawTokenAsync(rawToken);
        if (invitation is null || invitation.IsUsed || invitation.IsRevoked)
            return new ValidateInvitationResult { Valid = false, Expired = false };
        if (invitation.IsExpired)
            return new ValidateInvitationResult { Valid = false, Expired = true };

        var user = await _userManager.FindByIdAsync(invitation.UserId);
        return new ValidateInvitationResult
        {
            Valid      = true,
            Expired    = false,
            Email      = user?.Email,
            MemberName = await ResolveMemberNameAsync(user),
        };
    }

    // ── Accept ───────────────────────────────────────────────────────────────

    public async Task<(AppUser? User, string? Error)> AcceptAsync(string rawToken, string newPassword)
    {
        var invitation = await FindByRawTokenAsync(rawToken);
        if (invitation is null || invitation.IsUsed || invitation.IsRevoked)
            return (null, "This invitation link is invalid or has already been used.");
        if (invitation.IsExpired)
            return (null, "This invitation link has expired. Please ask for a new one.");

        var user = await _userManager.FindByIdAsync(invitation.UserId);
        if (user is null)
            return (null, "The account for this invitation no longer exists.");

        // Set the password — works whether or not one already exists, and enforces the policy.
        var resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
        var result     = await _userManager.ResetPasswordAsync(user, resetToken, newPassword);
        if (!result.Succeeded)
            return (null, string.Join(" ", result.Errors.Select(error => error.Description)));

        invitation.UsedAt   = DateTime.UtcNow;
        user.EmailConfirmed = true;
        user.IsActive       = true;
        await _userManager.UpdateAsync(user);
        await _db.SaveChangesAsync();

        _audit.Write(
            AuditActions.InvitationAccepted, AuditCategories.Security, LogLevelEnum.Information,
            entityName: nameof(AppUser), entityId: user.Id,
            summary: $"Invitation accepted — password set for {user.Email}",
            userId: user.Id, userEmail: user.Email);

        return (user, null);
    }

    // ── Send email ───────────────────────────────────────────────────────────

    public async Task SendEmailAsync(int memberId, string link)
    {
        var member = await _db.Members.FindAsync(memberId)
            ?? throw new InvalidOperationException($"Member {memberId} does not exist.");

        var name    = WebUtility.HtmlEncode(member.NickName ?? member.FirstName_en);
        var safeLink = WebUtility.HtmlEncode(link);
        var subject = "Your River Of Life Christian Church account invitation";
        var htmlBody =
            $"<p>Hi {name},</p>" +
            "<p>You've been invited to set up your account for the River Of Life Christian Church portal.</p>" +
            $"<p>Click the link below to set your password and sign in. This link expires in {InvitationLifetimeDays} days and can only be used once.</p>" +
            $"<p><a href=\"{safeLink}\">Set your password and sign in</a></p>" +
            "<p>If the button doesn't work, copy and paste this address into your browser:</p>" +
            $"<p>{safeLink}</p>";

        var result = await _emailService.SendAsync(new EmailMessage(
            MemberIds: new[] { memberId },
            Addresses: Array.Empty<string>(),
            Subject:   subject,
            HtmlBody:  htmlBody));

        if (result.SentCount == 0)
            throw new InvalidOperationException(
                result.Failures.Count > 0
                    ? $"Failed to send email: {result.Failures[0].Error}"
                    : "No email address on file for this member.");
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private Task<UserInvitation?> FindByRawTokenAsync(string rawToken)
    {
        if (string.IsNullOrWhiteSpace(rawToken))
            return Task.FromResult<UserInvitation?>(null);

        var hash = _tokenService.HashToken(rawToken);
        return _db.UserInvitations.FirstOrDefaultAsync(invitation => invitation.TokenHash == hash);
    }

    private async Task<string?> ResolveMemberNameAsync(AppUser? user)
    {
        if (user?.MemberId is not int memberId)
            return null;

        return await _db.Members
            .Where(member => member.Id == memberId)
            .Select(member => (member.NickName ?? member.FirstName_en) + " " + member.LastName_en)
            .FirstOrDefaultAsync();
    }

    /// <summary>32 cryptographically-random bytes as a URL-safe base64 string.</summary>
    private static string GenerateRawToken()
    {
        var bytes = RandomNumberGenerator.GetBytes(32);
        return Convert.ToBase64String(bytes)
            .Replace('+', '-')
            .Replace('/', '_')
            .TrimEnd('=');
    }
}
