using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Assets;
using Tiku.Application.Jobs;
using Tiku.Application.Storage;
using Tiku.Domain.Common;
using Tiku.Domain.Content;

namespace Tiku.Infrastructure.Assets;

public sealed partial class AssetManagementService
{
    public async Task<AssetUploadSignResult> SignUploadAsync(
        AssetManagementActor actor,
        AssetUploadSignCommand command,
        CancellationToken cancellationToken = default)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(command.FileName);
        ArgumentException.ThrowIfNullOrWhiteSpace(command.MimeType);

        var storageConfig = await ResolveObjectStorageConfigAsync(actor.TenantId, cancellationToken);
        var provider = storageConfig.Provider;
        var bucket = storageConfig.Bucket;
        var mimeType = objectStorageService.ValidateMimeType(command.MimeType.Trim());
        var fileSizeBytes = objectStorageService.ValidateFileSize(command.FileSizeBytes);
        var asset = await ResolveUploadAssetAsync(actor, command, provider, bucket, mimeType, fileSizeBytes,
            cancellationToken);
        var objectKey = objectStorageService.ValidateObjectKey(
            actor.TenantId,
            string.IsNullOrWhiteSpace(command.ObjectKey)
                ? CreateObjectKey(actor.TenantId, asset.Id, command.FileName)
                : command.ObjectKey.Trim());

        objectStorageService.AssertUploadProvider(provider);
        objectStorageService.AssertWritableLocation(new StorageAssetLocation(provider, bucket, objectKey));

        asset.StorageProvider = ToAssetStorageProvider(provider);
        asset.Bucket = bucket;
        asset.ObjectKey = objectKey;
        asset.FileName = command.FileName.Trim();
        asset.MimeType = mimeType;
        asset.FileSizeBytes = fileSizeBytes;
        asset.ChecksumSha256 = NormalizeChecksum(command.ChecksumSha256);
        asset.UploadStatus = AssetUploadStatus.Pending;
        asset.VerifiedAt = null;
        asset.VerifiedBy = null;
        asset.VerificationDetails = JsonDefaults.Object();
        asset.SecurityScanStatus = AssetSecurityScanStatus.Pending;
        asset.PreviewStatus = ResolveInitialPreviewStatus(asset.AssetType, mimeType);
        asset.UpdatedBy = actor.UserId;

        var expiresIn = ResolveUploadTtl(command.ExpiresInSeconds);
        var upload = await objectStorageService.SignUploadAsync(
            new ObjectStorageUploadSignRequest(
                actor.TenantId,
                provider,
                bucket,
                objectKey,
                asset.FileName,
                mimeType,
                fileSizeBytes,
                expiresIn,
                true),
            cancellationToken);

        await dbContext.SaveChangesAsync(cancellationToken);
        return new AssetUploadSignResult(ToItem(asset), upload);
    }

    public async Task<AssetUploadConfirmResult> ConfirmUploadAsync(
        AssetManagementActor actor,
        AssetUploadConfirmCommand command,
        CancellationToken cancellationToken = default)
    {
        var asset = await dbContext.ContentAssets
            .SingleOrDefaultAsync(
                item =>
                    item.TenantId == actor.TenantId &&
                    item.Id == command.AssetId &&
                    item.Status == ContentStatus.Active,
                cancellationToken);

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

        if (string.IsNullOrWhiteSpace(asset.ObjectKey) || string.IsNullOrWhiteSpace(asset.Bucket))
            throw new AssetManagementException("Asset does not have a writable object location.",
                "asset_location_missing");

        var accountedBytesBefore = AccountedStorageBytes(asset);
        var provider = ToObjectStorageProvider(asset.StorageProvider);
        var declaredMimeType = string.IsNullOrWhiteSpace(command.MimeType) ? asset.MimeType : command.MimeType.Trim();
        var declaredSize = command.FileSizeBytes ?? asset.FileSizeBytes;
        var declaredChecksum = NormalizeChecksum(command.ChecksumSha256) ?? asset.ChecksumSha256;
        var metadata = await objectStorageService.HeadObjectAsync(
            new ObjectStorageHeadRequest(
                actor.TenantId,
                provider,
                asset.Bucket,
                asset.ObjectKey,
                declaredMimeType,
                declaredSize,
                declaredChecksum),
            cancellationToken);

        asset.VerificationDetails = JsonSerializer.SerializeToElement(new
        {
            metadata.Exists,
            metadata.SizeBytes,
            metadata.MimeType,
            metadata.ChecksumSha256,
            metadata.ETag,
            metadata.LastModified,
            metadata.VerificationSource,
            declared = new
            {
                MimeType = declaredMimeType,
                FileSizeBytes = declaredSize,
                ChecksumSha256 = declaredChecksum
            }
        });

        if (!metadata.Exists)
        {
            asset.UploadStatus = AssetUploadStatus.Failed;
            asset.UpdatedBy = actor.UserId;
            await dbContext.SaveChangesAsync(cancellationToken);
            throw new AssetManagementException("Uploaded object was not found in object storage.",
                "asset_upload_missing");
        }

        if (metadata.SizeBytes is not { } verifiedSizeBytes)
        {
            asset.UploadStatus = AssetUploadStatus.Failed;
            asset.UpdatedBy = actor.UserId;
            await dbContext.SaveChangesAsync(cancellationToken);
            throw new AssetManagementException(
                "Object storage did not return a verified asset size.",
                "asset_upload_size_unverified");
        }

        asset.UploadStatus = AssetUploadStatus.Verified;
        asset.VerifiedAt = DateTimeOffset.UtcNow;
        asset.VerifiedBy = actor.UserId;
        asset.VerifiedSizeBytes = verifiedSizeBytes;
        asset.VerifiedChecksumSha256 = NormalizeChecksum(metadata.ChecksumSha256) ?? declaredChecksum;
        asset.MimeType = metadata.MimeType ?? declaredMimeType;
        asset.FileSizeBytes = verifiedSizeBytes;
        asset.SecurityScanStatus = AssetSecurityScanStatus.Pending;
        asset.UpdatedBy = actor.UserId;
        var accountedBytesAfter = AccountedStorageBytes(asset);
        await SaveWithStorageQuotaAdjustmentAsync(
            actor.TenantId,
            accountedBytesAfter - accountedBytesBefore,
            cancellationToken);

        await backgroundJobService.EnqueueAsync(
            new CreateBackgroundJobCommand(
                actor.TenantId,
                "asset_security_scan",
                JsonSerializer.SerializeToElement(new { assetId = asset.Id }),
                MaxRetries: 5,
                IdempotencyKey:
                $"asset:{asset.Id:N}:{asset.VerifiedChecksumSha256 ?? asset.VerifiedAt?.UtcTicks.ToString()}",
                IsSystemJob: true),
            cancellationToken);

        return new AssetUploadConfirmResult(ToItem(asset), metadata);
    }
}