using SkiaSharp;
using System.Collections.Concurrent;
using PlotLine.Data;
using PlotLine.Models;

namespace PlotLine.Services;

public interface IBookCoverService
{
    Task<BookCoverUploadResult> UploadCoverAsync(Book book, IFormFile upload);
    Task DeleteCoverFilesAsync(Book book);
}

public sealed record BookCoverUploadResult(bool Succeeded, string? ErrorMessage = null);

public sealed class BookCoverService(
    IUploadStorageService uploadStorage,
    IProjectCollaborationRepository collaboration,
    ISubscriptionService subscriptions,
    IUserFileRepository userFiles,
    IBookRepository books,
    ILogger<BookCoverService> logger) : IBookCoverService
{
    public const string PlaceholderPath = "/images/placeholders/book-cover.svg";
    private const long MaxUploadBytes = 10L * 1024L * 1024L;
    private const string ThumbnailFileName = "cover-thumb.webp";
    private const string StandardFileName = "cover-standard.webp";
    private static readonly HashSet<string> SupportedExtensions = new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp" };
    private static readonly ConcurrentDictionary<int, SemaphoreSlim> BookLocks = new();

    public async Task<BookCoverUploadResult> UploadCoverAsync(Book book, IFormFile upload)
    {
        var uploadLock = BookLocks.GetOrAdd(book.BookID, _ => new SemaphoreSlim(1, 1));
        await uploadLock.WaitAsync();
        try
        {
            return await UploadCoverCoreAsync(book, upload);
        }
        finally
        {
            uploadLock.Release();
        }
    }

    private async Task<BookCoverUploadResult> UploadCoverCoreAsync(Book book, IFormFile upload)
    {
        if (upload.Length <= 0)
        {
            return new(false, "Choose a cover image to upload.");
        }

        if (upload.Length > MaxUploadBytes)
        {
            return new(false, "Cover image must be 10 MB or smaller.");
        }

        if (!SupportedExtensions.Contains(Path.GetExtension(upload.FileName)))
        {
            return new(false, "Cover image must be a JPG, PNG or WebP file.");
        }

        var ownerUserId = await collaboration.GetOwnerUserIdAsync(book.ProjectID);
        if (!ownerUserId.HasValue)
        {
            return new(false, "Book owner could not be found.");
        }

        var storageLimit = await subscriptions.CanUploadFileAsync(ownerUserId.Value, EstimatedGeneratedSize(upload.Length));
        if (!storageLimit.Allowed)
        {
            return new(false, storageLimit.Message);
        }

        SKBitmap bitmap;
        try
        {
            await using var uploadStream = upload.OpenReadStream();
            using var memory = new MemoryStream();
            await uploadStream.CopyToAsync(memory);
            memory.Position = 0;

            using var codec = SKCodec.Create(memory) ?? throw new InvalidDataException();
            if (codec.EncodedFormat is not (SKEncodedImageFormat.Jpeg or SKEncodedImageFormat.Png or SKEncodedImageFormat.Webp))
            {
                return new(false, "Cover image must be a valid JPG, PNG or WebP file.");
            }

            memory.Position = 0;
            bitmap = SKBitmap.Decode(memory) ?? throw new InvalidDataException();
        }
        catch (Exception ex) when (ex is InvalidDataException or ArgumentException)
        {
            return new(false, "Cover image must be a valid JPG, PNG or WebP file.");
        }

        using (bitmap)
        {
            if (bitmap.Width <= 0 || bitmap.Height <= 0)
            {
                return new(false, "Cover image could not be read as a valid image.");
            }

            var uploadRoot = uploadStorage.GetDirectory("books", book.BookID.ToString());

            var version = Guid.NewGuid().ToString("N");
            var thumbnailTemp = Path.Combine(uploadRoot, $"cover-thumb-{version}.tmp");
            var standardTemp = Path.Combine(uploadRoot, $"cover-standard-{version}.tmp");
            var thumbnailPath = Path.Combine(uploadRoot, ThumbnailFileName);
            var standardPath = Path.Combine(uploadRoot, StandardFileName);
            var thumbnailStoragePath = uploadStorage.GetPublicPath("books", book.BookID.ToString(), ThumbnailFileName);
            var standardStoragePath = uploadStorage.GetPublicPath("books", book.BookID.ToString(), StandardFileName);

            try
            {
                SaveWebpVariant(bitmap, thumbnailTemp, 400, 600, 72);
                SaveWebpVariant(bitmap, standardTemp, 600, 900, 78);

                ReplaceFile(thumbnailTemp, thumbnailPath);
                ReplaceFile(standardTemp, standardPath);

                var oldThumbnailPath = book.CoverThumbnailPath;
                var oldStandardPath = book.CoverStandardPath;
                await books.UpdateCoverAsync(book.BookID, thumbnailStoragePath, standardStoragePath);
                await MarkPreviousLedgerRowsAsync(oldThumbnailPath);
                await MarkPreviousLedgerRowsAsync(oldStandardPath);
                await TrackCoverAsync(ownerUserId.Value, book.ProjectID, book.BookID, thumbnailStoragePath, "Book cover thumbnail", new FileInfo(thumbnailPath).Length);
                await TrackCoverAsync(ownerUserId.Value, book.ProjectID, book.BookID, standardStoragePath, "Book cover standard", new FileInfo(standardPath).Length);
                DeletePreviousPhysicalFile(oldThumbnailPath, thumbnailStoragePath);
                DeletePreviousPhysicalFile(oldStandardPath, standardStoragePath);
                return new(true);
            }
            catch (Exception ex)
            {
                TryDeleteFile(thumbnailTemp);
                TryDeleteFile(standardTemp);
                logger.LogWarning(ex, "Book cover upload failed for book {BookID}.", book.BookID);
                return new(false, "Cover image could not be processed. Try another image.");
            }
        }
    }

    public async Task DeleteCoverFilesAsync(Book book)
    {
        await MarkAndDeletePreviousAsync(book.CoverThumbnailPath, null);
        await MarkAndDeletePreviousAsync(book.CoverStandardPath, null);
    }

    private static void SaveWebpVariant(SKBitmap source, string path, int width, int height, int quality)
    {
        using var surface = SKSurface.Create(new SKImageInfo(width, height));
        var canvas = surface.Canvas;
        canvas.Clear(SKColors.Transparent);

        var sourceRatio = source.Width / (float)source.Height;
        var targetRatio = width / (float)height;
        SKRect sourceRect;

        if (sourceRatio > targetRatio)
        {
            var cropWidth = source.Height * targetRatio;
            var left = (source.Width - cropWidth) / 2f;
            sourceRect = new SKRect(left, 0, left + cropWidth, source.Height);
        }
        else
        {
            var cropHeight = source.Width / targetRatio;
            var top = (source.Height - cropHeight) / 2f;
            sourceRect = new SKRect(0, top, source.Width, top + cropHeight);
        }

        using var paint = new SKPaint { IsAntialias = true };
        using var sourceImage = SKImage.FromBitmap(source);
        canvas.DrawImage(sourceImage, sourceRect, new SKRect(0, 0, width, height), new SKSamplingOptions(SKCubicResampler.Mitchell), paint);
        canvas.Flush();

        using var image = surface.Snapshot();
        using var data = image.Encode(SKEncodedImageFormat.Webp, quality);
        using var output = File.Create(path);
        data.SaveTo(output);
    }

    private async Task TrackCoverAsync(int ownerUserId, int projectId, int bookId, string storagePath, string originalFileName, long fileSizeBytes)
    {
        await userFiles.CreateAsync(new UserFile
        {
            UserID = ownerUserId,
            ProjectID = projectId,
            EntityType = "BookCover",
            EntityID = bookId,
            FileName = Path.GetFileName(storagePath),
            OriginalFileName = originalFileName,
            ContentType = "image/webp",
            FileSizeBytes = fileSizeBytes,
            StoragePath = storagePath
        });
    }

    private async Task MarkPreviousLedgerRowsAsync(string? oldStoragePath)
    {
        if (string.IsNullOrWhiteSpace(oldStoragePath)
            || string.Equals(oldStoragePath, PlaceholderPath, StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        await userFiles.MarkDeletedByStoragePathAsync(oldStoragePath);
    }

    private async Task MarkAndDeletePreviousAsync(string? oldStoragePath, string? currentStoragePath)
    {
        await MarkPreviousLedgerRowsAsync(oldStoragePath);
        DeletePreviousPhysicalFile(oldStoragePath, currentStoragePath);
    }

    private void DeletePreviousPhysicalFile(string? oldStoragePath, string? currentStoragePath)
    {
        if (string.IsNullOrWhiteSpace(oldStoragePath)
            || string.Equals(oldStoragePath, currentStoragePath, StringComparison.OrdinalIgnoreCase)
            || string.Equals(oldStoragePath, PlaceholderPath, StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        uploadStorage.TryDeleteFile(oldStoragePath, "uploads/books");
    }

    private static void ReplaceFile(string tempPath, string finalPath)
    {
        if (File.Exists(finalPath))
        {
            File.Delete(finalPath);
        }

        File.Move(tempPath, finalPath);
    }

    private static void TryDeleteFile(string path)
    {
        if (!File.Exists(path))
        {
            return;
        }

        File.Delete(path);
    }

    private static long EstimatedGeneratedSize(long uploadBytes)
        => Math.Max(1, Math.Min(uploadBytes, MaxUploadBytes));
}
