using Microsoft.EntityFrameworkCore;
using Modules.User.Application.Models.Access;
using Modules.User.Application.Repositories;
using Modules.User.Database.Database;
using Modules.User.Database.Database.Entities;
using Modules.User.Domain.Factories;
using Modules.User.Domain.Factories.User;
using Modules.User.Domain.ValueObjects;
using ClientInfo = Modules.User.Domain.Entities.User.ClientInfo;

namespace Modules.User.Database.Repositories;

public class UserRepository(UserDbContext context) : IUserRepository
{
    private IQueryable<Database.Entities.User> GetUserQuery(bool asNoTracking = true)
    {
        var query = context.Users
            .Include(q => q.Account)
                .ThenInclude(q => q.Sessions)
            .Include(q => q.Account)
                .ThenInclude(q => q.Bans)
            .Include(q => q.Account)
                .ThenInclude(q => q.Roles)
                .ThenInclude(q => q.Role)
                // .ThenInclude(q => q.Permissions)
            .Include(q => q.Account)
                .ThenInclude(q => q.Permissions)
                // .ThenInclude(q => q.Permission)
            .AsSplitQuery();
        return asNoTracking ? query.AsNoTracking() : query;
    }
    
    public async Task<Domain.Entities.User.User?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        var dbUser = await GetUserQuery()
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        return dbUser == null ? null : MapToDomain(dbUser);
    }


    public async Task<bool> ExistsByEmailAsync(string email, CancellationToken cancellationToken)
    {
        return await context.Accounts
            .AnyAsync(q => q.Email == email.Trim().ToLower(), cancellationToken);
    }


    public async Task<HashedAccount?> TryGetHashedAccountAsync(string email, CancellationToken cancellationToken)
    {
        var normalized = email.Trim().ToLowerInvariant();
        var account = await context.Accounts
            .Where(a => a.Email == normalized)
            .Select(q => new HashedAccount(q.Id, q.HashedPassword))
            .SingleOrDefaultAsync(cancellationToken);
        return account;
    }

    public async Task<Domain.Entities.User.User?> GetByAccountIdAsync(Guid accountId, CancellationToken cancellationToken = default)
    {
        var dbUser = await GetUserQuery()
            .FirstOrDefaultAsync(x => x.Account.Id == accountId, cancellationToken);
        return dbUser == null ? null : MapToDomain(dbUser);
    }

    public Task AddAsync(Domain.Entities.User.User user, CancellationToken cancellationToken = default)
    {
        var dbUser = MapToEfNew(user);
        context.Users.Add(dbUser);
        return Task.CompletedTask;
    }

    public async Task SaveAsync(Domain.Entities.User.User user, CancellationToken cancellationToken = default)
    {
        var dbUser = await GetUserQuery(asNoTracking: false)
            .FirstAsync(x => x.Id == user.Id, cancellationToken);
        
        await context.Entry(dbUser.Account)
            .Collection(a => a.Sessions)
            .Query()
            .Include(q => q.ClientInfo)
            .LoadAsync(cancellationToken);
        // await context.Entry(dbUser.Account).Collection(a => a.Roles).LoadAsync(cancellationToken);
        // await context.Entry(dbUser.Account).Collection(a => a.Permissions).LoadAsync(cancellationToken);
        // await context.Entry(dbUser.Account).Collection(a => a.Bans).LoadAsync(cancellationToken);
        // ApplyDomainToEf(user, dbUser);
        await ApplyDomainToEf(user, dbUser, cancellationToken);
    }
    
    
    
    // ---------- Mapping ----------
    private static Domain.Entities.User.User MapToDomain(Database.Entities.User ef)
    {
        var email = Email.Create(ef.Account.Email);
        
        var sessions = ef.Account.Sessions
            .OrderBy(q => q.Id)
            .Select(q => SessionFactory.Load(q.Id, q.RefreshToken,
                ClientInfo.Create(q.ClientInfo.UserAgent, q.ClientInfo.Country, q.ClientInfo.Region),
                q.ExpiredDate, q.LastUpdate));
        
        var roles = ef.Account.Roles
            .Where(q => !q.RevokedAtUtc.HasValue)
            .OrderByDescending(q => q.GrantedAtUtc)
            .Select(q => RoleGrantFactory.Load(q.RoleId, q.GrantedAtUtc, q.IssuerId, q.GrantReason,
                q.RevokedAtUtc, q.RevokerId, q.RevokeReason));
        var permissions = ef.Account.Permissions
            .Where(q => !q.RevokedAtUtc.HasValue)
            .OrderByDescending(q => q.GrantedAtUtc)
            .Select(q => PermissionGrantFactory.Load(q.PermissionId, q.GrantedAtUtc, q.IssuerId, q.GrantReason,
                q.RevokedAtUtc, q.RevokerId, q.RevokeReason));
        
        var bans = ef.Account.Bans
            .OrderByDescending(q => q.ReleaseDate).ThenByDescending(q => q.BanDate)
            .Select(q => BanFactory.Load(q.Id, q.Reason, q.BanDate, q.ReleaseDate, q.IssuerId,
                q.UnbanIssuerId, q.UnbanDate, q.UnbanReason, q.Deleted));
        
        var account = AccountFactory.Load(ef.Account.Id, email, ef.Account.HashedPassword,
            sessions, roles, permissions, bans, ef.Account.Deleted);

        var user = UserFactory.Load(ef.Id, ef.NickName, ef.FirstName, ef.Patronymic, ef.LastName,
            ef.AvatarId, ef.RegionalSettings?.LanguageId, ef.BirthDate, account, ef.Deleted);

        return user;
    }

    private static Database.Entities.User MapToEfNew(Domain.Entities.User.User domain)
    {
        return new Database.Entities.User
        {
            Id = domain.Id,
            NickName = domain.NickName,
            FirstName = domain.FirstName,
            Patronymic = domain.Patronymic,
            LastName = domain.LastName,
            AvatarId = domain.Avatar?.Id,
            RegionalSettings = domain.Language is null ? null : new RegionalSettings { LanguageId = domain.Language.Id },
            BirthDate = domain.BirthDate,
            Account = new Account
            {
                Id = domain.Account.Id,
                UserId = domain.Id,
                Email = domain.Account.Email.Value,
                HashedPassword = domain.Account.HashedPassword,

                Sessions = [..domain.Account.Sessions.Select(s => new Session
                {
                    Id = s.Id,
                    AccountId = domain.Account.Id,
                    RefreshToken = s.RefreshToken,
                    ClientInfo = new Database.Entities.ClientInfo
                    {
                        UserAgent = s.ClientInfo.UserAgent,
                        Country = s.ClientInfo.Country,
                        Region = s.ClientInfo.Region
                    },
                    ExpiredDate = s.ExpiredDateUtc,
                    LastUpdate = s.LastUpdateUtc
                })],
                Roles = [..domain.Account.RoleGrants.Select(r => new AccountRole
                {
                    // Id = 
                    // AccountId = domain.Account.Id,
                    RoleId = r.RoleId,
                    IssuerId = r.IssuerId,
                    GrantedAtUtc = r.GrantedAtUtc,
                    RevokedAtUtc = null,
                })],
                Permissions = [..domain.Account.PermissionGrants.Select(p => new AccountPermission
                {
                    // Id = 
                    // AccountId = domain.Account.Id,
                    PermissionId = p.PermissionId,
                    IssuerId = p.IssuerId,
                    GrantedAtUtc = p.GrantedAtUtc,
                    RevokedAtUtc = null,
                })],
                Bans = [..domain.Account.Bans.Select(b => new AccountBan
                {
                    Id = b.Id,
                    AccountId = domain.Account.Id,
                    Reason = b.Reason,
                    BanDate = b.BanDate,
                    ReleaseDate = b.ReleaseDate,
                    IssuerId = b.BanIssuerId,
                    Deleted = b.Deleted,
                })],
                Deleted = domain.Account.Deleted,
            },
            Deleted = domain.Deleted,
        };
    }

    private async Task ApplyDomainToEf(Domain.Entities.User.User domain, Database.Entities.User ef, CancellationToken cancellationToken)
    {
        ef.NickName = domain.NickName;
        ef.FirstName = domain.FirstName;
        ef.Patronymic = domain.Patronymic;
        ef.LastName = domain.LastName;
        ef.AvatarId = domain.Avatar?.Id;
        ef.RegionalSettings ??= new RegionalSettings();
        ef.RegionalSettings.LanguageId = domain.Language?.Id;
        ef.BirthDate = domain.BirthDate;
        ef.Deleted = domain.Deleted;

        ef.Account.Email = domain.Account.Email.Value;
        ef.Account.HashedPassword = domain.Account.HashedPassword;
        ef.Account.Deleted = domain.Account.Deleted;

        var efSessions = ef.Account.Sessions.ToDictionary(s => s.Id, s => s);
        var domainSessions = domain.Account.Sessions.ToDictionary(s => s.Id, s => s);
        
        foreach (var es in ef.Account.Sessions.ToList())
        {
            if (!domainSessions.ContainsKey(es.Id))
                ef.Account.Sessions.Remove(es);
        }
        
        // var keepIds = domain.Account.Sessions.Select(s => s.Id).ToArray();
        // await context.Sessions
        //     .Where(s => s.AccountId == ef.Account.Id && !keepIds.Contains(s.Id))
        //     .ExecuteDeleteAsync(cancellationToken);

        foreach (var ds in domain.Account.Sessions)
        {
            if (!efSessions.TryGetValue(ds.Id, out var es))
            {
                es = new Session
                {
                    Id = ds.Id,
                    AccountId = ef.Account.Id,
                    ClientInfo = new Database.Entities.ClientInfo
                    {
                        UserAgent = ds.ClientInfo.UserAgent,
                        Country = ds.ClientInfo.Country,
                        Region = ds.ClientInfo.Region,
                    }
                };
                // ef.Account.Sessions.Add(es);
                context.Sessions.Add(es);
            }

            es.RefreshToken = ds.RefreshToken;
            es.ExpiredDate = ds.ExpiredDateUtc;
            es.LastUpdate = ds.LastUpdateUtc;
            es.ClientInfo ??= new Database.Entities.ClientInfo();
            es.ClientInfo.UserAgent = ds.ClientInfo.UserAgent;
            es.ClientInfo.Country = ds.ClientInfo.Country;
            es.ClientInfo.Region = ds.ClientInfo.Region;
        }
        
        var now = DateTime.UtcNow;
        var efRoles = ef.Account.Roles
            .Where(q => !q.RevokedAtUtc.HasValue)
            .ToDictionary(x => x.RoleId);
        var domainRoles = domain.Account.RoleGrants
            .Select(r => r.RoleId)
            .ToList();
        foreach (var dg in domain.Account.RoleGrants)
        {
            if (!efRoles.TryGetValue(dg.RoleId, out var er))
            {
                er = new AccountRole
                {
                    AccountId = ef.Account.Id,
                    RoleId = dg.RoleId,
                    GrantedAtUtc = dg.GrantedAtUtc,
                    IssuerId = dg.IssuerId,
                    RevokedAtUtc = null,
                };
                ef.Account.Roles.Add(er);
            }
        }
        foreach (var er in efRoles.Values)
        {
            if (!domainRoles.Contains(er.RoleId))
            {
                er.RevokedAtUtc = now;
            }
        }
        
        var efPermissions = ef.Account.Permissions
            .Where(q => !q.RevokedAtUtc.HasValue)
            .ToDictionary(p => p.PermissionId);
        var domainPermissions = domain.Account.PermissionGrants
            .Select(r => r.PermissionId)
            .ToList();
        foreach (var dg in domain.Account.PermissionGrants)
        {
            if (!efPermissions.TryGetValue(dg.PermissionId, out var ep))
            {
                ep = new AccountPermission
                {
                    AccountId = ef.Account.Id,
                    PermissionId = dg.PermissionId,
                    GrantedAtUtc = dg.GrantedAtUtc,
                    IssuerId = dg.IssuerId,
                    RevokedAtUtc = null,
                };
                ef.Account.Permissions.Add(ep);
            }
        }
        foreach (var er in efPermissions.Values)
        {
            if (!domainPermissions.Contains(er.PermissionId))
            {
                er.RevokedAtUtc = now;
            }
        }

        var efBans = ef.Account.Bans.ToDictionary(b => b.Id);
        foreach (var db in domain.Account.Bans)
        {
            if (!efBans.TryGetValue(db.Id, out var eb))
            {
                eb = new AccountBan
                {
                    Id = db.Id,
                    AccountId = ef.Account.Id,
                    IssuerId = db.BanIssuerId,
                };
                ef.Account.Bans.Add(eb);
            }
            eb.Reason = db.Reason;
            eb.BanDate = db.BanDate;
            eb.ReleaseDate = db.ReleaseDate;
            eb.Deleted = db.Deleted;
        }
    }
}