using Microsoft.AspNetCore.Components.Forms;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenArchival.Blazor.CustomComponents;

namespace OpenArchival.DataAccess.FileAccessManager;

public class FileAccessManager : IFileAccessManager
{
    private IDbContextFactory<ApplicationDbContext> _contextFactory { get; set; }
    IOptions<FileUploadOptions> _fileUploadOptions { get; set; }
    ILogger<FileAccessManager> _logger { get; set; }
    private readonly IImageThumbnailer _thumbnailer;

    public FileAccessManager(
        IDbContextFactory<ApplicationDbContext> contextFactory, 
        IOptions<FileUploadOptions> fileOptions,
        ILogger<FileAccessManager> logger,
        IImageThumbnailer thumbnailer
        )
    {
        _contextFactory = contextFactory;
        _fileUploadOptions = fileOptions;
        _logger = logger;
        _thumbnailer = thumbnailer;
    }

    /// <summary>
    /// Queries the file listing from the database based on the id, then deletes the file from the disk and
    /// the file listing from the database.
    ///
    /// </summary>
    /// <param name="id"></param>
    /// <returns>
    /// Returns true if the file is deleted successfully
    /// returns false if the file cannot be found in the database
    /// </returns>
    /// <exception cref="IOException">Thrown if there is an IOException while trying to delete the file.</exception>
    public async Task<bool> DeleteFileAsync(int id)
    {
        await using var context = await _contextFactory.CreateDbContextAsync();

        FilePathListing? existingListing = await context.ArtifactFilePaths
            .Where(listing=>listing.Id == id)
            .SingleOrDefaultAsync();

        if (existingListing == null)
        {
            return false; 
        }

        var filename = existingListing.Path;
        var smallThumb = existingListing.SmallThumbnailPath;
        var largeThumb = existingListing.LargeThumbnailPath;

        // Delete from the database before we delete from the disk to avoid someone trying to read it
        // before it deletes
        await context.ArtifactFilePaths.Where(listing => listing.Id == id).ExecuteDeleteAsync();

        try
        {
            if (File.Exists(filename))
            {
                File.Delete(filename);
            }
            
            if (!string.IsNullOrEmpty(smallThumb) && File.Exists(smallThumb))
            {
                File.Delete(smallThumb);
            }

            if (!string.IsNullOrEmpty(largeThumb) && File.Exists(largeThumb))
            {
                File.Delete(largeThumb);
            }
        } 
        catch (IOException e)
        {
            throw new IOException("Could not delete file(s) from the disk!", e);
        }

        return true;
    }

    /// <summary>
    /// Uploads the given browser file to the disk and creates a corresponding database entry.
    /// </summary>
    /// <param name="browserFile"></param>
    /// <returns>Returns the FilePathListing object that was inserted to the database</returns>
    /// <exception cref="IOException">Throws wrapped IOException if the FileStream cannot be constructed.</exception>
    public async Task<FilePathListing> UploadFileAsync(IBrowserFile browserFile)
    {
        var diskFileName = $"{Guid.NewGuid()}{Path.GetExtension(browserFile.Name)}";
        var destinationPath = Path.Combine(_fileUploadOptions.Value.UploadFolderPath, diskFileName);

        await using var browserUploadStream = browserFile.OpenReadStream(maxAllowedSize: _fileUploadOptions.Value.MaxUploadSizeBytes);


        try { 
            await using var outFileStream = new FileStream(destinationPath, FileMode.Create);
            await browserUploadStream.CopyToAsync(outFileStream);
        } catch (IOException e) 
        {
            throw new IOException("Failed to open file stream to write file to disk", e);
        } 

        // Create the new entity
        var newFile = new FilePathListing() { Path = destinationPath, OriginalName = Path.GetFileName(browserFile.Name) };

        // Generate thumbnails if it is a supported image
        if (_thumbnailer.IsSupportedImage(destinationPath))
        {
            try
            {
                var result = await _thumbnailer.GenerateThumbnailsAsync(destinationPath);
                newFile.SmallThumbnailPath = result.SmallThumbnailPath;
                newFile.LargeThumbnailPath = result.LargeThumbnailPath;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Failed to generate thumbnails for {Path}", destinationPath);
                // We don't throw here, as we still want to save the original file
            }
        }

        await using var context = await _contextFactory.CreateDbContextAsync();
        context.ArtifactFilePaths.Add(newFile);

        // Save the file entry
        await context.SaveChangesAsync();

        return newFile;
    }

    public async Task<FilePathListing?> GetFile(int id)
    {
        await using var context = await _contextFactory.CreateDbContextAsync();

        FilePathListing? existingListing = await context.ArtifactFilePaths.FirstOrDefaultAsync(a => a.Id == id);

        return existingListing;
    }

    public async Task<(int Processed, int Failed)> BackfillThumbnailsAsync()
    {
        await using var context = await _contextFactory.CreateDbContextAsync();

        var listingsToProcess = await context.ArtifactFilePaths
            .Where(f => string.IsNullOrEmpty(f.SmallThumbnailPath) || string.IsNullOrEmpty(f.LargeThumbnailPath))
            .ToListAsync();

        int processed = 0;
        int failed = 0;

        foreach (var listing in listingsToProcess)
        {
            if (!File.Exists(listing.Path))
            {
                _logger.LogWarning("File not found on disk during backfill: {Path} (ID: {Id})", listing.Path, listing.Id);
                failed++;
                continue;
            }

            if (_thumbnailer.IsSupportedImage(listing.Path))
            {
                try
                {
                    var result = await _thumbnailer.GenerateThumbnailsAsync(listing.Path);
                    listing.SmallThumbnailPath = result.SmallThumbnailPath;
                    listing.LargeThumbnailPath = result.LargeThumbnailPath;
                    processed++;
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Failed to generate thumbnails during backfill for {Path} (ID: {Id})", listing.Path, listing.Id);
                    failed++;
                }
            }
        }

        if (processed > 0)
        {
            await context.SaveChangesAsync();
        }

        return (processed, failed);
    }
}
