using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Auth;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
using Tiku.Domain.Identity;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Auth;

internal sealed class OwnerActivationService(
    ITenantExecutionScope tenantExecutionScope,
    ITenantRuntimeCacheInvalidator runtimeCacheInvalidator) : IOwnerActivationService
{
    public async Task CompleteAsync(
        CompleteOwnerActivationRequest request,
        CancellationToken cancellationToken = default)
    {
        await CompleteCoreAsync(request, null, null, null, null, false, cancellationToken);
    }

    public async Task<AuthenticationResult> CompleteAndAuthenticateAsync(
        CompleteOwnerActivationRequest request,
        Guid expectedTenantId,
        string expectedHost,
        string? ipAddress,
        string? userAgent,
        CancellationToken cancellationToken = default)
    {
        var result = await CompleteCoreAsync(
                         request, expectedTenantId, expectedHost, ipAddress, userAgent, true, cancellationToken)
                     ?? throw Error("Owner activation session could not be established.", "owner_activation_failed");
        await runtimeCacheInvalidator.InvalidateAsync(expectedTenantId, cancellationToken);
        return result;
    }

    private Task<AuthenticationResult?> CompleteCoreAsync(
        CompleteOwnerActivationRequest request,
        Guid? expectedTenantId,
        string? expectedHost,
        string? ipAddress,
        string? userAgent,
        bool authenticate,
        CancellationToken cancellationToken)
    {
        return tenantExecutionScope.ExecuteAsync(
            new SystemScopeRequest(
                null,
                SystemScopeCallerType.Anonymous,
                nameof(OwnerActivationService),
                "Complete tenant owner activation",
                request.ActivationId.ToString("N"),
                true),
            async (services, token) =>
            {
                var dbContext = services.GetRequiredService<TikuDbContext>();
                var grant = await dbContext.TenantOwnerActivationGrants
                                .SingleOrDefaultAsync(value => value.Id == request.ActivationId, token)
                            ?? throw Error("Owner activation was not found.", "owner_activation_invalid");
                var now = DateTimeOffset.UtcNow;
                var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(request.Token)))
                    .ToLowerInvariant();
                if (grant.ConsumedAt.HasValue)
                    throw Error("Owner activation was already consumed.", "owner_activation_consumed");

                if (grant.RevokedAt.HasValue || grant.ExpiresAt <= now || !CryptographicOperations.FixedTimeEquals(
                        Convert.FromHexString(grant.TokenHash),
                        Convert.FromHexString(tokenHash)))
                    throw Error("Owner activation is invalid or expired.", "owner_activation_invalid");

                if (expectedTenantId.HasValue)
                {
                    if (grant.TenantId != expectedTenantId || grant.DomainId is not { } domainId)
                        throw Error("Owner activation does not belong to this tenant host.",
                            "owner_activation_host_mismatch");

                    var normalizedHost = expectedHost!.Trim().TrimEnd('.').ToLowerInvariant();
                    var domainMatches = await dbContext.TenantDomains.AsNoTracking().AnyAsync(value =>
                        value.Id == domainId && value.TenantId == grant.TenantId && value.IsPrimary &&
                        value.Status == TenantDomainStatus.Active && value.Host == normalizedHost, token);
                    if (!domainMatches)
                        throw Error("Owner activation does not belong to this tenant host.",
                            "owner_activation_host_mismatch");
                }

                var claimed = await dbContext.TenantOwnerActivationGrants
                    .Where(value => value.Id == grant.Id && value.ConsumedAt == null && value.RevokedAt == null &&
                                    value.ExpiresAt > now && value.TokenHash == tokenHash)
                    .ExecuteUpdateAsync(setters => setters
                        .SetProperty(value => value.ConsumedAt, now)
                        .SetProperty(value => value.UpdatedAt, now), token);
                if (claimed != 1)
                    throw Error("Owner activation is invalid or already consumed.", "owner_activation_consumed");

                var userManager = services.GetRequiredService<UserManager<User>>();
                var user = await userManager.FindByIdAsync(grant.UserId.ToString())
                           ?? throw Error("Owner account was not found.", "owner_activation_invalid");
                if (await userManager.HasPasswordAsync(user))
                    throw Error("Owner account was already activated.", "owner_activation_consumed");

                var result = await userManager.AddPasswordAsync(user, request.NewPassword);
                if (!result.Succeeded)
                    throw Error(
                        string.Join("; ", result.Errors.Select(error => error.Description)),
                        "owner_activation_password_invalid");

                user.ForcePasswordChange = false;
                var updateResult = await userManager.UpdateAsync(user);
                if (!updateResult.Succeeded)
                    throw Error("Owner account activation could not be completed.", "owner_activation_failed");

                await userManager.UpdateSecurityStampAsync(user);
                dbContext.AuditLogs.Add(new AuditLog
                {
                    TenantId = grant.TenantId,
                    ActorUserId = grant.UserId,
                    Action = "tenant.owner.activated",
                    TargetType = "users",
                    TargetId = grant.UserId.ToString()
                });

                AuthenticationResult? authentication = null;
                if (authenticate)
                {
                    var tenant = await dbContext.Tenants.SingleAsync(value => value.Id == grant.TenantId, token);
                    var membership = await dbContext.TenantMemberships.AsNoTracking().SingleAsync(value =>
                        value.TenantId == grant.TenantId && value.UserId == grant.UserId &&
                        value.Status == MembershipStatus.Active, token);
                    var tokens = await services.GetRequiredService<IAuthSessionStore>().IssueAsync(
                        new AuthSessionIssueRequest(
                            user.Id, user.Phone, user.Email, user.SecurityStamp ?? string.Empty,
                            AuthRealm.Tenant, tenant.Id, "owner_activation", ipAddress, userAgent), token);
                    dbContext.AuthLoginEvents.Add(new AuthLoginEvent
                    {
                        TenantId = tenant.Id,
                        UserId = user.Id,
                        Provider = "owner_activation",
                        Identifier = user.Email ?? user.Phone,
                        Result = AuthLoginResult.Success,
                        IpAddress = ipAddress,
                        UserAgent = userAgent
                    });
                    authentication = new AuthenticationResult(
                        AuthenticationStatus.Authenticated,
                        new AuthenticatedUser(
                            user.Id, user.Phone, user.Email, user.Name, AuthRealm.Tenant,
                            new TenantMembershipSummary(tenant.Id, tenant.Name, membership.Role, membership.Status),
                            tokens));
                }

                await dbContext.SaveChangesAsync(token);
                return authentication;
            },
            cancellationToken);
    }

    private static OwnerActivationException Error(string message, string code)
    {
        return new OwnerActivationException(message, code);
    }
}