using System.Security.Cryptography;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using WishNinja.Configuration;
using WishNinja.Data;
using WishNinja.Data.Entities;

namespace WishNinja.Services;

public record InviteResult(bool Succeeded, string? Error = null, string? AcceptUrl = null);

/// <summary>
/// Manages the admin-invite-only onboarding flow: an admin creates an invite, the recipient
/// follows an emailed single-use link to set a display name + password, which creates the account.
/// </summary>
public class InviteService(
    IDbContextFactory<ApplicationDbContext> dbFactory,
    UserManager<ApplicationUser> userManager,
    IAppEmailSender emailSender,
    IOptions<WishNinjaOptions> options,
    ILogger<InviteService> logger)
{
    private readonly WishNinjaOptions _options = options.Value;

    /// <summary>Creates an invite, emails the accept link, and returns the link (for admin display).</summary>
    public async Task<InviteResult> CreateInviteAsync(string email, string role, string invitedByUserId)
    {
        email = email.Trim();
        if (string.IsNullOrWhiteSpace(email))
            return new InviteResult(false, "Email is required.");

        if (await userManager.FindByEmailAsync(email) is not null)
            return new InviteResult(false, "A user with that email already exists.");

        await using var db = await dbFactory.CreateDbContextAsync();

        // Supersede any outstanding, unaccepted invites for the same email.
        var stale = await db.Invites
            .Where(i => i.Email == email && i.AcceptedAt == null)
            .ToListAsync();
        db.Invites.RemoveRange(stale);

        var rawToken = GenerateToken();
        var invite = new Invite
        {
            Email = email,
            TokenHash = HashToken(rawToken),
            Role = role,
            InvitedByUserId = invitedByUserId,
            ExpiresAt = DateTimeOffset.UtcNow.AddHours(_options.Invites.ExpiryHours),
        };
        db.Invites.Add(invite);
        await db.SaveChangesAsync();

        var acceptUrl = $"{_options.BaseUrl.TrimEnd('/')}/account/accept-invite?token={Uri.EscapeDataString(rawToken)}";
        await emailSender.SendEmailAsync(email, "You're invited to WishNinja",
            $"You've been invited to join WishNinja. <a href='{acceptUrl}'>Click here to set up your account</a>. This link expires in {_options.Invites.ExpiryHours / 24} days.");

        logger.LogInformation("Invite created for {Email} by {Admin}", email, invitedByUserId);
        return new InviteResult(true, AcceptUrl: acceptUrl);
    }

    /// <summary>Returns the matching pending, unexpired invite for a raw token, or null.</summary>
    public async Task<Invite?> GetValidInviteAsync(string rawToken)
    {
        if (string.IsNullOrWhiteSpace(rawToken)) return null;
        var hash = HashToken(rawToken);
        await using var db = await dbFactory.CreateDbContextAsync();
        var invite = await db.Invites.FirstOrDefaultAsync(i => i.TokenHash == hash);
        if (invite is null || invite.IsAccepted || invite.ExpiresAt < DateTimeOffset.UtcNow)
            return null;
        return invite;
    }

    /// <summary>Redeems an invite, creating the user account. Single-use.</summary>
    public async Task<IdentityResult> AcceptInviteAsync(string rawToken, string displayName, string password)
    {
        var invite = await GetValidInviteAsync(rawToken);
        if (invite is null)
            return IdentityResult.Failed(new IdentityError { Description = "This invite link is invalid or has expired." });

        var user = new ApplicationUser
        {
            UserName = invite.Email,
            Email = invite.Email,
            EmailConfirmed = true, // ownership proven by following the emailed link
            DisplayName = string.IsNullOrWhiteSpace(displayName) ? invite.Email : displayName.Trim(),
        };

        var create = await userManager.CreateAsync(user, password);
        if (!create.Succeeded)
            return create;

        await userManager.AddToRoleAsync(user, invite.Role);

        // Mark single-use within a fresh context (the one in GetValidInviteAsync is disposed).
        await using var db = await dbFactory.CreateDbContextAsync();
        await db.Invites.Where(i => i.Id == invite.Id)
            .ExecuteUpdateAsync(s => s.SetProperty(i => i.AcceptedAt, DateTimeOffset.UtcNow));

        logger.LogInformation("Invite accepted, user created: {Email}", invite.Email);
        return IdentityResult.Success;
    }

    private static string GenerateToken()
    {
        var bytes = RandomNumberGenerator.GetBytes(32);
        return Convert.ToBase64String(bytes)
            .Replace('+', '-').Replace('/', '_').TrimEnd('=');
    }

    private static string HashToken(string rawToken)
    {
        var bytes = SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rawToken));
        return Convert.ToBase64String(bytes);
    }
}
