using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Auth;
using Tiku.Application.Jobs;
using Tiku.Application.Security;
using Tiku.Application.Storage;
using Tiku.Application.Tenancy;
using Tiku.Domain.Content;
using Tiku.Domain.Operations;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Tenancy;

internal sealed class TenantLifecycleService(
    TikuDbContext dbContext,
    IBackgroundJobQueue backgroundJobService,
    IAuthSessionStore sessionStore,
    ITenantRuntimeCacheInvalidator runtimeCacheInvalidator,
    ITenantPublicCacheInvalidator publicCacheInvalidator,
    ITenantFeatureCacheInvalidator featureCacheInvalidator,
    IAuthorizationStateInvalidator authorizationStateInvalidator,
    IObjectStorageService objectStorageService) : ITenantLifecycleService
{
    private static readonly TimeSpan RecentExportWindow = TimeSpan.FromHours(24);

    public async Task<TenantArchivePreview> PreviewArchiveAsync(
        Guid tenantId,
        CancellationToken cancellationToken = default)
    {
        var tenant = await dbContext.Tenants.AsNoTracking()
                         .SingleOrDefaultAsync(item => item.Id == tenantId, cancellationToken)
                     ?? throw new TenantLifecycleException("tenant_not_found", "Tenant was not found.");
        var blockers = new List<string>();
        if (tenant.Status == TenantStatus.Archived) blockers.Add("tenant_already_archived");
        if (await dbContext.BackgroundJobs.AnyAsync(item =>
                    item.TenantId == tenantId &&
                    item.Status == BackgroundJobStatus.Processing,
                cancellationToken))
            blockers.Add("processing_background_jobs");
        var recentExport = await HasRecentExportAsync(tenantId, cancellationToken);
        if (!recentExport) blockers.Add("recent_successful_export_required");
        return new TenantArchivePreview(tenantId, blockers.Count == 0, recentExport, blockers);
    }

    public async Task<TenantLifecycleOperationItem> CreateExportAsync(
        Guid tenantId,
        Guid actorUserId,
        CancellationToken cancellationToken = default)
    {
        await RequireTenantAsync(tenantId, cancellationToken);
        var operation = CreateOperation(tenantId, actorUserId, TenantLifecycleOperationType.Export, null);
        dbContext.TenantLifecycleOperations.Add(operation);
        await dbContext.SaveChangesAsync(cancellationToken);
        await backgroundJobService.EnqueueAsync(
            new CreateBackgroundJobCommand(
                tenantId,
                "tenant_export",
                JsonSerializer.SerializeToElement(new { operationId = operation.Id }),
                MaxRetries: 3,
                IdempotencyKey: $"tenant-export:{operation.Id:N}",
                IsSystemJob: true),
            cancellationToken);
        return ToItem(operation);
    }

    public async Task<TenantLifecycleOperationItem?> GetOperationAsync(
        Guid tenantId,
        Guid operationId,
        CancellationToken cancellationToken = default)
    {
        var operation = await dbContext.TenantLifecycleOperations.AsNoTracking().SingleOrDefaultAsync(
            item => item.TenantId == tenantId && item.Id == operationId,
            cancellationToken);
        return operation is null ? null : ToItem(operation);
    }

    public async Task<ObjectStorageSignedUrl> SignExportDownloadAsync(
        Guid tenantId,
        Guid operationId,
        CancellationToken cancellationToken = default)
    {
        var operation = await dbContext.TenantLifecycleOperations.AsNoTracking().SingleOrDefaultAsync(
                            item => item.TenantId == tenantId && item.Id == operationId &&
                                    item.OperationType == TenantLifecycleOperationType.Export &&
                                    item.Status == TenantLifecycleOperationStatus.Succeeded,
                            cancellationToken) ??
                        throw new TenantLifecycleException("tenant_export_not_ready", "Tenant export is not ready.");
        var asset = operation.ExportAssetId.HasValue
            ? await dbContext.ContentAssets.AsNoTracking().SingleOrDefaultAsync(
                item => item.TenantId == tenantId && item.Id == operation.ExportAssetId.Value,
                cancellationToken)
            : null;
        if (asset is null || string.IsNullOrWhiteSpace(asset.ObjectKey))
            throw new TenantLifecycleException("tenant_export_not_ready", "Tenant export asset was not found.");
        return await objectStorageService.SignDownloadAsync(
            new ObjectStorageDownloadSignRequest(
                tenantId,
                ToProvider(asset.StorageProvider),
                asset.Bucket,
                asset.ObjectKey,
                TimeSpan.FromMinutes(15),
                asset.CdnUrl,
                asset.FileName),
            cancellationToken);
    }

    public async Task<TenantLifecycleOperationItem> ArchiveAsync(
        Guid tenantId,
        Guid actorUserId,
        string reason,
        CancellationToken cancellationToken = default)
    {
        var preview = await PreviewArchiveAsync(tenantId, cancellationToken);
        if (!preview.CanArchive)
            throw new TenantLifecycleException("tenant_archive_blocked", string.Join(',', preview.Blockers));
        var tenant = await RequireTenantAsync(tenantId, cancellationToken);
        var operation = CreateOperation(tenantId, actorUserId, TenantLifecycleOperationType.Archive, reason);
        operation.Status = TenantLifecycleOperationStatus.Succeeded;
        operation.StartedAt = operation.CompletedAt = DateTimeOffset.UtcNow;
        tenant.Status = TenantStatus.Archived;
        await dbContext.TenantDomains.Where(item => item.TenantId == tenantId)
            .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.Status, TenantDomainStatus.Disabled),
                cancellationToken);
        dbContext.TenantLifecycleOperations.Add(operation);
        AddAudit(tenantId, actorUserId, "tenant.archived", tenantId, reason);
        await dbContext.SaveChangesAsync(cancellationToken);
        await RevokeTenantSessionsAsync(tenantId, cancellationToken);
        await InvalidateAsync(tenantId, cancellationToken);
        return ToItem(operation);
    }

    public async Task<TenantLifecycleOperationItem> RestoreAsync(
        Guid tenantId,
        Guid actorUserId,
        string reason,
        CancellationToken cancellationToken = default)
    {
        var tenant = await RequireTenantAsync(tenantId, cancellationToken);
        if (tenant.Status != TenantStatus.Archived)
            throw new TenantLifecycleException("tenant_not_archived", "Only archived tenants can be restored.");
        var operation = CreateOperation(tenantId, actorUserId, TenantLifecycleOperationType.Restore, reason);
        operation.Status = TenantLifecycleOperationStatus.Succeeded;
        operation.StartedAt = operation.CompletedAt = DateTimeOffset.UtcNow;
        tenant.Status = TenantStatus.Suspended;
        await dbContext.TenantDomains.Where(item => item.TenantId == tenantId)
            .ExecuteUpdateAsync(setters => setters.SetProperty(item => item.Status, TenantDomainStatus.Pending),
                cancellationToken);
        dbContext.TenantLifecycleOperations.Add(operation);
        AddAudit(tenantId, actorUserId, "tenant.restored_suspended", tenantId, reason);
        await dbContext.SaveChangesAsync(cancellationToken);
        await InvalidateAsync(tenantId, cancellationToken);
        return ToItem(operation);
    }

    public async Task<TenantLifecycleOperationItem> TransferOwnerAsync(
        Guid tenantId,
        Guid actorUserId,
        Guid targetUserId,
        string reason,
        CancellationToken cancellationToken = default)
    {
        await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
        var tenant = await RequireTenantAsync(tenantId, cancellationToken);
        var target = await dbContext.TenantMemberships.SingleOrDefaultAsync(item =>
                item.TenantId == tenantId && item.UserId == targetUserId && item.Status == MembershipStatus.Active,
            cancellationToken) ?? throw new TenantLifecycleException(
            "tenant_owner_target_not_active_member",
            "New owner must be an existing active tenant member.");
        var previousOwnerId = tenant.OwnerUserId;
        if (previousOwnerId == targetUserId)
            throw new TenantLifecycleException("tenant_owner_unchanged", "Target user is already the tenant owner.");
        var previous = previousOwnerId.HasValue
            ? await dbContext.TenantMemberships.SingleOrDefaultAsync(item =>
                    item.TenantId == tenantId && item.UserId == previousOwnerId.Value,
                cancellationToken)
            : null;
        if (previous is not null) previous.Role = TenantRole.TenantAdmin;
        target.Role = TenantRole.TenantOwner;
        tenant.OwnerUserId = targetUserId;

        var ownerRoleId = await dbContext.TenantBackendRoles
            .Where(item => item.TenantId == tenantId && item.Code == "tenant_owner")
            .Select(item => (Guid?)item.Id)
            .SingleOrDefaultAsync(cancellationToken);
        if (ownerRoleId.HasValue)
        {
            if (previousOwnerId.HasValue)
                await dbContext.TenantBackendUserRoles
                    .Where(item =>
                        item.TenantId == tenantId && item.UserId == previousOwnerId.Value &&
                        item.RoleId == ownerRoleId.Value)
                    .ExecuteDeleteAsync(cancellationToken);
            if (!await dbContext.TenantBackendUserRoles.AnyAsync(item =>
                        item.TenantId == tenantId && item.UserId == targetUserId && item.RoleId == ownerRoleId.Value,
                    cancellationToken))
                dbContext.TenantBackendUserRoles.Add(new TenantBackendUserRole
                {
                    TenantId = tenantId,
                    UserId = targetUserId,
                    RoleId = ownerRoleId.Value
                });
        }

        var operation = CreateOperation(tenantId, actorUserId, TenantLifecycleOperationType.OwnerTransfer, reason);
        operation.TargetUserId = targetUserId;
        operation.Status = TenantLifecycleOperationStatus.Succeeded;
        operation.StartedAt = operation.CompletedAt = DateTimeOffset.UtcNow;
        operation.Result = JsonSerializer.SerializeToElement(new { previousOwnerId, newOwnerId = targetUserId });
        dbContext.TenantLifecycleOperations.Add(operation);
        AddAudit(tenantId, actorUserId, "tenant.owner_transferred", targetUserId, reason);
        await dbContext.SaveChangesAsync(cancellationToken);
        await transaction.CommitAsync(cancellationToken);
        await authorizationStateInvalidator.BumpScopeAsync(AuthRealm.Tenant, tenantId, cancellationToken);
        if (previousOwnerId.HasValue)
            await authorizationStateInvalidator.InvalidateMembershipAsync(tenantId, previousOwnerId.Value,
                cancellationToken);
        await authorizationStateInvalidator.InvalidateMembershipAsync(tenantId, targetUserId, cancellationToken);
        await InvalidateAsync(tenantId, cancellationToken);
        return ToItem(operation);
    }

    private Task<bool> HasRecentExportAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        var cutoff = DateTimeOffset.UtcNow - RecentExportWindow;
        return dbContext.TenantLifecycleOperations.AnyAsync(item =>
                item.TenantId == tenantId &&
                item.OperationType == TenantLifecycleOperationType.Export &&
                item.Status == TenantLifecycleOperationStatus.Succeeded &&
                item.CompletedAt >= cutoff,
            cancellationToken);
    }

    private async Task<Tenant> RequireTenantAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        return await dbContext.Tenants.SingleOrDefaultAsync(item => item.Id == tenantId, cancellationToken) ??
               throw new TenantLifecycleException("tenant_not_found", "Tenant was not found.");
    }

    private async Task RevokeTenantSessionsAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        var userIds = await dbContext.TenantMemberships.AsNoTracking()
            .Where(item => item.TenantId == tenantId)
            .Select(item => item.UserId)
            .Distinct()
            .ToArrayAsync(cancellationToken);
        foreach (var userId in userIds)
            await sessionStore.RevokeRealmAsync(userId, AuthRealm.Tenant, tenantId, "tenant_archived",
                cancellationToken);
    }

    private async Task InvalidateAsync(Guid tenantId, CancellationToken cancellationToken)
    {
        await runtimeCacheInvalidator.InvalidateAsync(tenantId, cancellationToken);
        await publicCacheInvalidator.InvalidateAsync(tenantId, cancellationToken);
        await featureCacheInvalidator.InvalidateAsync(tenantId, cancellationToken);
        await authorizationStateInvalidator.InvalidateTenantAsync(tenantId, cancellationToken);
    }

    private static TenantLifecycleOperation CreateOperation(
        Guid tenantId,
        Guid actorUserId,
        TenantLifecycleOperationType type,
        string? reason)
    {
        return new TenantLifecycleOperation
        {
            TenantId = tenantId,
            RequestedBy = actorUserId,
            OperationType = type,
            Reason = reason?.Trim()
        };
    }

    private void AddAudit(Guid tenantId, Guid actorUserId, string action, Guid targetId, string reason)
    {
        dbContext.AuditLogs.Add(new AuditLog
        {
            TenantId = tenantId,
            ActorUserId = actorUserId,
            Action = action,
            TargetType = "tenant",
            TargetId = targetId.ToString(),
            Details = JsonSerializer.SerializeToElement(new { reason })
        });
    }

    private static string ToProvider(AssetStorageProvider provider)
    {
        return provider switch
        {
            AssetStorageProvider.AliyunOss => ObjectStorageProviders.AliyunOss,
            AssetStorageProvider.LocalDev => ObjectStorageProviders.LocalDev,
            _ => ObjectStorageProviders.ExternalUrl
        };
    }

    private static TenantLifecycleOperationItem ToItem(TenantLifecycleOperation operation)
    {
        return new TenantLifecycleOperationItem(
            operation.Id,
            operation.TenantId,
            operation.OperationType,
            operation.Status,
            operation.RequestedBy,
            operation.TargetUserId,
            operation.ExportAssetId,
            operation.Reason,
            operation.LastError,
            operation.CreatedAt,
            operation.CompletedAt);
    }
}