using System.Globalization;
using System.Security.Cryptography;
using System.Text.Json;
using DysonNetwork.Sphere.Email;
using DysonNetwork.Sphere.Pages.Emails;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Resources.Localization;
using DysonNetwork.Sphere.Resources.Pages.Emails;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using NodaTime;

namespace DysonNetwork.Sphere.Account;

public class MagicSpellService(
    AppDatabase db,
    EmailService email,
    IConfiguration configuration,
    ILogger<MagicSpellService> logger,
    IStringLocalizer<Localization.EmailResource> localizer
)
{
    public async Task<MagicSpell> CreateMagicSpell(
        Account account,
        MagicSpellType type,
        Dictionary<string, object> meta,
        Instant? expiredAt = null,
        Instant? affectedAt = null,
        bool preventRepeat = false
    )
    {
        if (preventRepeat)
        {
            var now = SystemClock.Instance.GetCurrentInstant();
            var existingSpell = await db.MagicSpells
                .Where(s => s.AccountId == account.Id)
                .Where(s => s.Type == type)
                .Where(s => s.ExpiresAt == null || s.ExpiresAt > now)
                .FirstOrDefaultAsync();

            if (existingSpell != null)
            {
                throw new InvalidOperationException($"Account already has an active magic spell of type {type}");
            }
        }

        var spellWord = _GenerateRandomString(128);
        var spell = new MagicSpell
        {
            Spell = spellWord,
            Type = type,
            ExpiresAt = expiredAt,
            AffectedAt = affectedAt,
            AccountId = account.Id,
            Meta = meta
        };

        db.MagicSpells.Add(spell);
        await db.SaveChangesAsync();

        return spell;
    }

    public async Task NotifyMagicSpell(MagicSpell spell, bool bypassVerify = false)
    {
        var contact = await db.AccountContacts
            .Where(c => c.Account.Id == spell.AccountId)
            .Where(c => c.Type == AccountContactType.Email)
            .Where(c => c.VerifiedAt != null || bypassVerify)
            .OrderByDescending(c => c.IsPrimary)
            .Include(c => c.Account)
            .FirstOrDefaultAsync();
        if (contact is null) throw new ArgumentException("Account has no contact method that can use");

        var link = $"{configuration.GetValue<string>("BaseUrl")}/spells/{Uri.EscapeDataString(spell.Spell)}";

        logger.LogInformation("Sending magic spell... {Link}", link);

        var accountLanguage = await db.Accounts
            .Where(a => a.Id == spell.AccountId)
            .Select(a => a.Language)
            .FirstOrDefaultAsync();
        AccountService.SetCultureInfo(accountLanguage);

        try
        {
            switch (spell.Type)
            {
                case MagicSpellType.AccountActivation:
                    await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
                        contact.Account.Nick,
                        contact.Content,
                        localizer["EmailLandingTitle"],
                        new LandingEmailModel
                        {
                            Name = contact.Account.Name,
                            Link = link
                        }
                    );
                    break;
                case MagicSpellType.AccountRemoval:
                    await email.SendTemplatedEmailAsync<AccountDeletionEmail, AccountDeletionEmailModel>(
                        contact.Account.Nick,
                        contact.Content,
                        localizer["EmailAccountDeletionTitle"],
                        new AccountDeletionEmailModel
                        {
                            Name = contact.Account.Name,
                            Link = link
                        }
                    );
                    break;
                case MagicSpellType.AuthPasswordReset:
                    await email.SendTemplatedEmailAsync<PasswordResetEmail, PasswordResetEmailModel>(
                        contact.Account.Nick,
                        contact.Content,
                        localizer["EmailAccountDeletionTitle"],
                        new PasswordResetEmailModel
                        {
                            Name = contact.Account.Name,
                            Link = link
                        }
                    );
                    break;
                case MagicSpellType.ContactVerification:
                    if (spell.Meta["contact_method"] is not string contactMethod)
                        throw new InvalidOperationException("Contact method is not found.");
                    await email.SendTemplatedEmailAsync<ContactVerificationEmail, ContactVerificationEmailModel>(
                        contact.Account.Nick,
                        contactMethod!,
                        localizer["EmailContactVerificationTitle"],
                        new ContactVerificationEmailModel
                        {
                            Name = contact.Account.Name,
                            Link = link
                        }
                    );
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }
        catch (Exception err)
        {
            logger.LogError($"Error sending magic spell (${spell.Spell})... {err}");
        }
    }

    public async Task ApplyMagicSpell(MagicSpell spell)
    {
        switch (spell.Type)
        {
            case MagicSpellType.AuthPasswordReset:
                throw new ArgumentException(
                    "For password reset spell, please use the ApplyPasswordReset method instead."
                );
            case MagicSpellType.AccountRemoval:
                var account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId);
                if (account is null) break;
                db.Accounts.Remove(account);
                break;
            case MagicSpellType.AccountActivation:
                var contactMethod = (spell.Meta["contact_method"] as JsonElement? ?? default).ToString();
                var contact = await
                    db.AccountContacts.FirstOrDefaultAsync(c =>
                        c.Content == contactMethod
                    );
                if (contact is not null)
                {
                    contact.VerifiedAt = SystemClock.Instance.GetCurrentInstant();
                    db.Update(contact);
                }

                account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId);
                if (account is not null)
                {
                    account.ActivatedAt = SystemClock.Instance.GetCurrentInstant();
                    db.Update(account);
                }

                var defaultGroup = await db.PermissionGroups.FirstOrDefaultAsync(g => g.Key == "default");
                if (defaultGroup is not null && account is not null)
                {
                    db.PermissionGroupMembers.Add(new PermissionGroupMember
                    {
                        Actor = $"user:{account.Id}",
                        Group = defaultGroup
                    });
                }

                break;
            case MagicSpellType.ContactVerification:
                var verifyContactMethod = (spell.Meta["contact_method"] as JsonElement? ?? default).ToString();
                var verifyContact = await db.AccountContacts
                    .FirstOrDefaultAsync(c => c.Content == verifyContactMethod);
                if (verifyContact is not null)
                {
                    verifyContact.VerifiedAt = SystemClock.Instance.GetCurrentInstant();
                    db.Update(verifyContact);
                }

                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        db.Remove(spell);
        await db.SaveChangesAsync();
    }

    public async Task ApplyPasswordReset(MagicSpell spell, string newPassword)
    {
        if (spell.Type != MagicSpellType.AuthPasswordReset)
            throw new ArgumentException("This spell is not a password reset spell.");

        var passwordFactor = await db.AccountAuthFactors
            .Include(f => f.Account)
            .Where(f => f.Type == AccountAuthFactorType.Password && f.Account.Id == spell.AccountId)
            .FirstOrDefaultAsync();
        if (passwordFactor is null)
        {
            var account = await db.Accounts.FirstOrDefaultAsync(c => c.Id == spell.AccountId);
            if (account is null) throw new InvalidOperationException("Both account and auth factor was not found.");
            passwordFactor = new AccountAuthFactor
            {
                Type = AccountAuthFactorType.Password,
                Account = account,
                Secret = newPassword
            }.HashSecret();
            db.AccountAuthFactors.Add(passwordFactor);
        }
        else
        {
            passwordFactor.Secret = newPassword;
            passwordFactor.HashSecret();
            db.Update(passwordFactor);
        }

        await db.SaveChangesAsync();
    }

    private static string _GenerateRandomString(int length)
    {
        using var rng = RandomNumberGenerator.Create();
        var randomBytes = new byte[length];
        rng.GetBytes(randomBytes);

        var base64String = Convert.ToBase64String(randomBytes);

        return base64String.Substring(0, length);
    }
}