using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Assets;
using Tiku.Application.Storage;
using Tiku.Domain.Commerce;
using Tiku.Domain.Content;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;

namespace Tiku.Infrastructure.Assets;

public sealed class AssetAccessService(
    TikuDbContext dbContext,
    IObjectStorageService objectStorageService) : IAssetAccessService
{
    private static readonly TimeSpan DefaultDownloadTtl = TimeSpan.FromMinutes(15);
    private static readonly TimeSpan DefaultPreviewTtl = TimeSpan.FromMinutes(10);

    public Task<AssetAccessResultModel> DownloadAsync(
        AssetAccessRequest request,
        CancellationToken cancellationToken = default)
    {
        return AccessAsync(request, AssetAccessType.Download, AssetAccessDisposition.Attachment, cancellationToken);
    }

    public Task<AssetAccessResultModel> PreviewAsync(
        AssetAccessRequest request,
        CancellationToken cancellationToken = default)
    {
        return AccessAsync(request, AssetAccessType.Preview, AssetAccessDisposition.Inline, cancellationToken);
    }

    private async Task<AssetAccessResultModel> AccessAsync(
        AssetAccessRequest request,
        AssetAccessType accessType,
        AssetAccessDisposition disposition,
        CancellationToken cancellationToken)
    {
        var asset = await dbContext.ContentAssets
            .SingleOrDefaultAsync(
                item =>
                    item.TenantId == request.TenantId &&
                    item.Id == request.AssetId &&
                    item.Status == ContentStatus.Active,
                cancellationToken);

        if (asset is null) throw new AssetAccessException("Asset was not found.", "ASSET_NOT_FOUND");

        try
        {
            var access = await ResolveAccessAsync(request, asset, cancellationToken);
            AssertPublishedAsset(asset);
            if (accessType == AssetAccessType.Preview) AssertPreviewable(asset);

            var ttl = ResolveTtl(request, accessType, asset.Visibility);
            var signedUrl = await objectStorageService.SignDownloadAsync(
                new ObjectStorageDownloadSignRequest(
                    request.TenantId,
                    ToStorageProvider(asset.StorageProvider),
                    asset.Bucket,
                    accessType == AssetAccessType.Preview
                        ? asset.PreviewObjectKey ?? asset.ObjectKey
                        : asset.ObjectKey,
                    ttl,
                    accessType == AssetAccessType.Preview
                        ? asset.PreviewUrl ?? asset.CdnUrl
                        : asset.CdnUrl,
                    asset.FileName ?? asset.Title,
                    disposition == AssetAccessDisposition.Inline ? "inline" : "attachment"),
                cancellationToken);

            if (accessType == AssetAccessType.Download) asset.DownloadCount++;

            dbContext.ContentAssetAccessEvents.Add(CreateAccessEvent(
                request,
                asset,
                accessType,
                disposition,
                AssetAccessResult.Granted,
                signedUrl,
                null,
                access));
            await dbContext.SaveChangesAsync(cancellationToken);

            return new AssetAccessResultModel(
                ToSummary(asset),
                access,
                signedUrl,
                CreateWatermark(request, asset, accessType, signedUrl));
        }
        catch (Exception exception) when (ShouldAuditDenied(exception))
        {
            dbContext.ContentAssetAccessEvents.Add(CreateAccessEvent(
                request,
                asset,
                accessType,
                disposition,
                AssetAccessResult.Denied,
                null,
                DenyCode(exception),
                null));
            await dbContext.SaveChangesAsync(cancellationToken);
            throw;
        }
    }

    private async Task<AssetAccessPrincipal> ResolveAccessAsync(
        AssetAccessRequest request,
        ContentAsset asset,
        CancellationToken cancellationToken)
    {
        if (asset.Visibility == ContentVisibility.Public || asset.IsPublic)
            return new AssetAccessPrincipal(request.UserId, false, false);

        if (asset.Visibility == ContentVisibility.Hidden)
            throw new AssetAccessException("Asset is hidden.", "ASSET_HIDDEN");

        if (!request.UserId.HasValue) throw new AssetAccessException("Authentication is required.", "AUTH_REQUIRED");

        var isMember = await dbContext.TenantMemberships
            .AnyAsync(
                membership =>
                    membership.TenantId == request.TenantId &&
                    membership.UserId == request.UserId.Value &&
                    membership.Status == MembershipStatus.Active,
                cancellationToken);

        if (!isMember)
            throw new AssetAccessException("Tenant membership is required for this asset.",
                "ASSET_MEMBERSHIP_REQUIRED");

        if (asset.Visibility == ContentVisibility.Members) return new AssetAccessPrincipal(request.UserId, true, false);

        var hasSvip = await HasSvipAccessAsync(request, asset, cancellationToken);
        if (!hasSvip)
            throw new AssetAccessException("SVIP entitlement is required for this asset.", "ASSET_SVIP_REQUIRED");

        return new AssetAccessPrincipal(request.UserId, true, true);
    }

    private Task<bool> HasSvipAccessAsync(
        AssetAccessRequest request,
        ContentAsset asset,
        CancellationToken cancellationToken)
    {
        var now = DateTimeOffset.UtcNow;
        return dbContext.Entitlements.AnyAsync(
            entitlement =>
                entitlement.TenantId == request.TenantId &&
                entitlement.UserId == request.UserId!.Value &&
                entitlement.EntitlementType == "svip" &&
                entitlement.Status == EntitlementStatus.Active &&
                entitlement.StartsAt <= now &&
                (entitlement.ExpiresAt == null || entitlement.ExpiresAt > now) &&
                (entitlement.ScopeType == EntitlementScopeType.Tenant ||
                 (asset.RegionId != null &&
                  entitlement.ScopeType == EntitlementScopeType.Region &&
                  entitlement.ScopeId == asset.RegionId) ||
                 (asset.SubjectId != null &&
                  entitlement.ScopeType == EntitlementScopeType.Subject &&
                  entitlement.ScopeId == asset.SubjectId)),
            cancellationToken);
    }

    private static void AssertPublishedAsset(ContentAsset asset)
    {
        if (!string.IsNullOrWhiteSpace(asset.ObjectKey) && asset.UploadStatus != AssetUploadStatus.Verified)
            throw new AssetAccessException("Asset upload has not been verified.", "ASSET_UPLOAD_NOT_VERIFIED");

        if (asset.SecurityScanStatus is not (AssetSecurityScanStatus.Passed or AssetSecurityScanStatus.NotRequired))
            throw new AssetAccessException("Asset security scan has not passed.", "ASSET_SECURITY_SCAN_NOT_PASSED");
    }

    private static void AssertPreviewable(ContentAsset asset)
    {
        var mimeType = asset.MimeType?.Split(';', StringSplitOptions.TrimEntries)[0].ToLowerInvariant() ?? string.Empty;
        var previewable = asset.AssetType is ContentAssetType.Pdf or ContentAssetType.Image ||
                          mimeType == "application/pdf" ||
                          mimeType.StartsWith("image/", StringComparison.Ordinal);
        if (!previewable)
            throw new AssetAccessException("Asset type does not support inline preview.",
                "ASSET_PREVIEW_NOT_SUPPORTED");
    }

    private static TimeSpan ResolveTtl(
        AssetAccessRequest request,
        AssetAccessType accessType,
        ContentVisibility visibility)
    {
        var fallback = accessType == AssetAccessType.Preview ? DefaultPreviewTtl : DefaultDownloadTtl;
        var requested = request.RequestedExpiresIn ?? fallback;
        var max = visibility == ContentVisibility.Public ? TimeSpan.FromHours(2) : TimeSpan.FromMinutes(15);
        if (requested <= TimeSpan.Zero) return fallback;

        return requested <= max ? requested : max;
    }

    private static ContentAssetAccessEvent CreateAccessEvent(
        AssetAccessRequest request,
        ContentAsset asset,
        AssetAccessType accessType,
        AssetAccessDisposition disposition,
        AssetAccessResult result,
        ObjectStorageSignedUrl? signedUrl,
        string? denyCode,
        AssetAccessPrincipal? access)
    {
        return new ContentAssetAccessEvent
        {
            TenantId = request.TenantId,
            AssetId = asset.Id,
            UserId = request.UserId,
            ActorRole = ResolveActorRole(access),
            AccessType = accessType,
            Visibility = asset.Visibility.ToString(),
            AssetType = asset.AssetType.ToString(),
            StorageProvider = asset.StorageProvider.ToString(),
            Disposition = disposition,
            ExpiresInSeconds = signedUrl is null ? null : (int)signedUrl.ExpiresIn.TotalSeconds,
            SignatureMode = signedUrl?.SignatureMode,
            Result = result,
            DenyCode = denyCode,
            IpAddress = request.IpAddress,
            UserAgent = request.UserAgent,
            Metadata = JsonSerializer.SerializeToElement(new
            {
                asset.Title,
                asset.FileName,
                signedUrl?.Method
            })
        };
    }

    private static AssetAccessActorRole ResolveActorRole(AssetAccessPrincipal? access)
    {
        return access?.UserId.HasValue == true ? AssetAccessActorRole.Student : AssetAccessActorRole.Anonymous;
    }

    private static ContentAssetAccessSummary ToSummary(ContentAsset asset)
    {
        return new ContentAssetAccessSummary(
            asset.Id,
            asset.AssetType,
            asset.Title,
            asset.FileName,
            asset.PreviewUrl,
            asset.UploadStatus,
            asset.SecurityScanStatus,
            asset.PreviewStatus,
            asset.Visibility);
    }

    private static JsonElement CreateWatermark(
        AssetAccessRequest request,
        ContentAsset asset,
        AssetAccessType accessType,
        ObjectStorageSignedUrl signedUrl)
    {
        return JsonSerializer.SerializeToElement(new
        {
            request.TenantId,
            assetId = asset.Id,
            request.UserId,
            accessType = accessType.ToString(),
            signedUrl.ExpiresAt,
            asset.Title,
            asset.FileName
        });
    }

    private static bool ShouldAuditDenied(Exception exception)
    {
        return exception is AssetAccessException or ObjectStorageException or ObjectStorageNotConfiguredException;
    }

    private static string DenyCode(Exception exception)
    {
        return exception switch
        {
            AssetAccessException accessException => accessException.Code,
            ObjectStorageException storageException => storageException.Code,
            _ => "ASSET_ACCESS_DENIED"
        };
    }

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