using System.Security.Cryptography;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Auth;

public sealed class AuthSessionStore(
    TikuDbContext dbContext,
    ITokenService tokenService,
    IOptions<JwtOptions> options) : IAuthSessionStore
{
    private readonly JwtOptions options = options.Value;

    public string GenerateRefreshToken(AuthRealm realm, Guid? tenantId, Guid sessionId)
    {
        var realmCode = realm == AuthRealm.Tenant ? "t" : "p";
        var tenant = tenantId?.ToString("N") ?? "-";
        return $"v2.{realmCode}.{tenant}.{sessionId:N}.{Base64UrlEncoder.Encode(RandomNumberGenerator.GetBytes(64))}";
    }

    public bool TryParseRefreshToken(string refreshToken, out RefreshTokenLocator locator)
    {
        locator = default;
        var parts = refreshToken?.Split('.', 5, StringSplitOptions.None) ?? [];
        if (parts.Length != 5 || parts[0] != "v2" || parts[4].Length < 64 ||
            !Guid.TryParseExact(parts[3], "N", out var sessionId))
        {
            return false;
        }

        if (parts[1] == "p" && parts[2] == "-")
        {
            locator = new RefreshTokenLocator(AuthRealm.Platform, null, sessionId);
            return true;
        }

        if (parts[1] == "t" && Guid.TryParseExact(parts[2], "N", out var tenantId))
        {
            locator = new RefreshTokenLocator(AuthRealm.Tenant, tenantId, sessionId);
            return true;
        }

        return false;
    }

    public string HashRefreshToken(string refreshToken) =>
        Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(refreshToken))).ToLowerInvariant();

    public async Task<AuthTokenPair> IssueAsync(
        AuthSessionIssueRequest request,
        CancellationToken cancellationToken = default)
    {
        ValidateRealm(request.Realm, request.TenantId);
        var session = CreateSession(request, Guid.NewGuid());
        var refreshToken = GenerateRefreshToken(session.Realm, session.TenantId, session.Id);
        session.TokenHash = HashRefreshToken(refreshToken);
        dbContext.AuthSessions.Add(session);
        await dbContext.SaveChangesAsync(cancellationToken);
        return CreatePair(request, session, refreshToken);
    }

    public async Task<AuthTokenPair> RotateAsync(
        string refreshToken,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken = default)
    {
        if (!TryParseRefreshToken(refreshToken, out var locator))
        {
            throw new SessionRevokedException();
        }

        var tokenHash = HashRefreshToken(refreshToken);
        var now = DateTimeOffset.UtcNow;
        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
        var current = await dbContext.AuthSessions.SingleOrDefaultAsync(
            item => item.Id == locator.SessionId && item.Realm == locator.Realm &&
                    item.TenantId == locator.TenantId && item.TokenHash == tokenHash,
            cancellationToken);
        if (current is null)
        {
            throw new SessionRevokedException();
        }

        if (current.RevokedAt.HasValue || current.ReplacedBySessionId.HasValue || current.ExpiresAt <= now)
        {
            await RevokeFamilyCoreAsync(current.TokenFamilyId, "refresh_token_reuse", now, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
            throw new SessionRevokedException();
        }

        var user = await dbContext.Users.SingleOrDefaultAsync(item => item.Id == current.UserId, cancellationToken);
        if (user is null || user.Status != UserStatus.Active ||
            !string.Equals(user.SecurityStamp, current.SecurityStamp, StringComparison.Ordinal))
        {
            await RevokeFamilyCoreAsync(current.TokenFamilyId, "identity_state_changed", now, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
            throw new SessionRevokedException();
        }

        try
        {
            await AssertRealmAccessAsync(
                current.Realm, current.TenantId, current.UserId, cancellationToken);
        }
        catch (TenantAccessDeniedException)
        {
            await RevokeFamilyCoreAsync(current.TokenFamilyId, "realm_access_revoked", now, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
            throw new SessionRevokedException();
        }
        var nextId = Guid.NewGuid();
        var updated = await dbContext.AuthSessions
            .Where(item => item.Id == current.Id && item.RevokedAt == null && item.ReplacedBySessionId == null)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.RevokedAt, now)
                .SetProperty(item => item.RevokedReason, "rotated")
                .SetProperty(item => item.ReplacedBySessionId, nextId), cancellationToken);
        if (updated != 1)
        {
            await RevokeFamilyCoreAsync(current.TokenFamilyId, "refresh_token_reuse", now, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
            throw new SessionRevokedException();
        }

        var request = new AuthSessionIssueRequest(
            user.Id, user.Phone, user.Email, user.SecurityStamp ?? string.Empty,
            current.Realm, current.TenantId, "refresh",
            ipAddress, userAgent, current.TokenFamilyId, current.Id);
        var next = CreateSession(request, nextId);
        var nextToken = GenerateRefreshToken(next.Realm, next.TenantId, next.Id);
        next.TokenHash = HashRefreshToken(nextToken);
        dbContext.AuthSessions.Add(next);
        await dbContext.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);
        return CreatePair(request, next, nextToken);
    }

    public async Task<AuthSessionValidationResult?> ValidateAccessSessionAsync(
        Guid sessionId,
        Guid userId,
        AuthRealm realm,
        Guid? tenantId,
        CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var state = await (
                from session in dbContext.AuthSessions.AsNoTracking()
                join user in dbContext.Users.AsNoTracking() on session.UserId equals user.Id
                where session.Id == sessionId &&
                      session.UserId == userId &&
                      session.Realm == realm &&
                      session.TenantId == tenantId &&
                      session.RevokedAt == null &&
                      session.ExpiresAt > now
                select new
                {
                    UserStatus = user.Status,
                    UserSecurityStamp = user.SecurityStamp,
                    SessionSecurityStamp = session.SecurityStamp,
                    TenantAllowed = realm != AuthRealm.Tenant ||
                        (tenantId != null &&
                         dbContext.Tenants.Any(item => item.Id == tenantId && item.Status == TenantStatus.Active) &&
                         dbContext.TenantMemberships.Any(item =>
                             item.TenantId == tenantId &&
                             item.UserId == userId &&
                             item.Status == MembershipStatus.Active)),
                    PlatformAllowed = realm != AuthRealm.Platform ||
                        (from userRole in dbContext.PlatformBackendUserRoles
                         join role in dbContext.PlatformBackendRoles on userRole.RoleId equals role.Id
                         join binding in dbContext.PlatformBackendRolePermissions on role.Id equals binding.RoleId
                         join permission in dbContext.BackendPermissions on binding.PermissionCode equals permission.Code
                         where userRole.UserId == userId &&
                               role.Status == BackendRoleStatus.Active &&
                               (permission.Area == BackendPermissionArea.Platform || permission.Area == BackendPermissionArea.Both)
                         select permission.Id).Any()
                })
            .SingleOrDefaultAsync(cancellationToken);
        if (state is null ||
            state.UserStatus != UserStatus.Active ||
            !string.Equals(state.UserSecurityStamp, state.SessionSecurityStamp, StringComparison.Ordinal) ||
            !state.TenantAllowed ||
            !state.PlatformAllowed)
        {
            return null;
        }

        return new AuthSessionValidationResult(userId, realm, tenantId);
    }

    public async Task RevokeFamilyAsync(string refreshToken, string reason, CancellationToken cancellationToken = default)
    {
        if (!TryParseRefreshToken(refreshToken, out var locator))
        {
            return;
        }

        var hash = HashRefreshToken(refreshToken);
        var session = await dbContext.AuthSessions.AsNoTracking().SingleOrDefaultAsync(
            item => item.Id == locator.SessionId && item.TokenHash == hash, cancellationToken);
        if (session is not null)
        {
            await RevokeFamilyCoreAsync(session.TokenFamilyId, reason, DateTimeOffset.UtcNow, cancellationToken);
        }
    }

    public async Task RevokeAllAsync(Guid userId, string reason, CancellationToken cancellationToken = default)
    {
        var now = DateTimeOffset.UtcNow;
        var count = await dbContext.AuthSessions.Where(item => item.UserId == userId && item.RevokedAt == null)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.RevokedAt, DateTimeOffset.UtcNow)
                .SetProperty(item => item.RevokedReason, reason), cancellationToken);
        if (count > 0)
        {
            dbContext.AuditLogs.Add(new AuditLog
            {
                ActorUserId = userId,
                Action = "auth.sessions.revoked_all",
                TargetType = "user",
                TargetId = userId.ToString(),
                Details = System.Text.Json.JsonSerializer.SerializeToElement(new { reason, count, revokedAt = now })
            });
            await dbContext.SaveChangesAsync(cancellationToken);
        }
    }

    public async Task RevokeRealmAsync(
        Guid userId,
        AuthRealm realm,
        Guid? tenantId,
        string reason,
        CancellationToken cancellationToken = default)
    {
        ValidateRealm(realm, tenantId);
        var now = DateTimeOffset.UtcNow;
        var count = await dbContext.AuthSessions
            .Where(item => item.UserId == userId && item.Realm == realm && item.TenantId == tenantId && item.RevokedAt == null)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.RevokedAt, now)
                .SetProperty(item => item.RevokedReason, reason), cancellationToken);
        if (count > 0)
        {
            dbContext.AuditLogs.Add(new AuditLog
            {
                TenantId = tenantId,
                ActorUserId = userId,
                Action = "auth.sessions.realm_revoked",
                TargetType = "user",
                TargetId = userId.ToString(),
                Details = System.Text.Json.JsonSerializer.SerializeToElement(new { realm, reason, count, revokedAt = now })
            });
            await dbContext.SaveChangesAsync(cancellationToken);
        }
    }

    private AuthSession CreateSession(AuthSessionIssueRequest request, Guid sessionId) => new()
    {
        Id = sessionId,
        Realm = request.Realm,
        TenantId = request.TenantId,
        UserId = request.UserId,
        TokenFamilyId = request.TokenFamilyId ?? sessionId,
        ParentSessionId = request.ParentSessionId,
        SecurityStamp = request.SecurityStamp,
        Provider = request.Provider,
        ExpiresAt = DateTimeOffset.UtcNow.AddDays(options.RefreshTokenDays),
        IpAddress = request.IpAddress,
        UserAgent = request.UserAgent
    };

    private AuthTokenPair CreatePair(AuthSessionIssueRequest request, AuthSession session, string refreshToken)
    {
        var access = tokenService.CreateAccessToken(
            request.UserId, session.Id, request.Phone, request.Email,
            request.Realm, request.TenantId);
        return new AuthTokenPair(access.Token, refreshToken, access.ExpiresAt, session.ExpiresAt);
    }

    private async Task AssertRealmAccessAsync(
        AuthRealm realm,
        Guid? tenantId,
        Guid userId,
        CancellationToken cancellationToken)
    {
        if (realm == AuthRealm.Tenant && tenantId.HasValue)
        {
            var active = await dbContext.Tenants.AnyAsync(item => item.Id == tenantId && item.Status == TenantStatus.Active, cancellationToken) &&
                         await dbContext.TenantMemberships.AnyAsync(item => item.TenantId == tenantId && item.UserId == userId && item.Status == MembershipStatus.Active, cancellationToken);
            if (active)
            {
                return;
            }
        }
        else if (realm == AuthRealm.Platform)
        {
            var active = await (
                from userRole in dbContext.PlatformBackendUserRoles
                join role in dbContext.PlatformBackendRoles on userRole.RoleId equals role.Id
                join binding in dbContext.PlatformBackendRolePermissions on role.Id equals binding.RoleId
                join permission in dbContext.BackendPermissions on binding.PermissionCode equals permission.Code
                where userRole.UserId == userId && role.Status == BackendRoleStatus.Active &&
                      (permission.Area == BackendPermissionArea.Platform || permission.Area == BackendPermissionArea.Both)
                select permission.Id).AnyAsync(cancellationToken);
            if (active) return;
        }

        throw new TenantAccessDeniedException();
    }

    private async Task<int> RevokeFamilyCoreAsync(Guid familyId, string reason, DateTimeOffset now, CancellationToken cancellationToken)
    {
        var owner = await dbContext.AuthSessions.AsNoTracking()
            .Where(item => item.TokenFamilyId == familyId)
            .Select(item => new { item.UserId, item.TenantId })
            .FirstOrDefaultAsync(cancellationToken);
        var count = await dbContext.AuthSessions.Where(item => item.TokenFamilyId == familyId && item.RevokedAt == null)
            .ExecuteUpdateAsync(setters => setters
                .SetProperty(item => item.RevokedAt, now)
                .SetProperty(item => item.RevokedReason, reason), cancellationToken);
        if (count > 0 && owner is not null)
        {
            dbContext.AuditLogs.Add(new AuditLog
            {
                TenantId = owner.TenantId,
                ActorUserId = owner.UserId,
                Action = "auth.session_family.revoked",
                TargetType = "auth_session_family",
                TargetId = familyId.ToString(),
                Details = System.Text.Json.JsonSerializer.SerializeToElement(new { reason, count, revokedAt = now })
            });
            await dbContext.SaveChangesAsync(cancellationToken);
        }

        return count;
    }

    private static void ValidateRealm(AuthRealm realm, Guid? tenantId)
    {
        if ((realm == AuthRealm.Tenant) != tenantId.HasValue)
        {
            throw new ArgumentException("Tenant sessions require a tenant and platform sessions must not have one.");
        }
    }
}
