using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore;
using Meezi.Core.Entities;
using Meezi.Infrastructure.Data;

namespace Meezi.API.Services;

public interface IMediaStorageService
{
    Task<string?> SaveMenuImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveMenuVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveTableImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveTableVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveCafeLogoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveCafeCoverAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveMenuModel3dAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveMenuModel3dFromBytesAsync(string cafeId, byte[] glbBytes, CancellationToken cancellationToken = default);
    Task<string?> SaveReviewPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
    Task<string?> SaveCafeGalleryPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default);
}

public class MediaStorageService : IMediaStorageService
{
    private static readonly HashSet<string> ImageMime = new(StringComparer.OrdinalIgnoreCase)
    {
        "image/jpeg", "image/png", "image/webp"
    };

    private static readonly HashSet<string> VideoMime = new(StringComparer.OrdinalIgnoreCase)
    {
        "video/mp4", "video/webm", "video/quicktime"
    };

    private const long MaxImageBytes = 5 * 1024 * 1024;
    private const long MaxVideoBytes = 25 * 1024 * 1024;
    private const long MaxModel3dBytes = 8 * 1024 * 1024;

    private static readonly HashSet<string> Model3dMime = new(StringComparer.OrdinalIgnoreCase)
    {
        "model/gltf-binary", "application/octet-stream"
    };

    private readonly IWebHostEnvironment _env;
    private readonly ILogger<MediaStorageService> _logger;
    private readonly IServiceScopeFactory _scopeFactory;

    public MediaStorageService(
        IWebHostEnvironment env,
        ILogger<MediaStorageService> logger,
        IServiceScopeFactory scopeFactory)
    {
        _env = env;
        _logger = logger;
        _scopeFactory = scopeFactory;
    }

    public Task<string?> SaveMenuImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "menu_img", ImageMime, MaxImageBytes, cancellationToken);

    public Task<string?> SaveMenuVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "menu_vid", VideoMime, MaxVideoBytes, cancellationToken);

    public Task<string?> SaveTableImageAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "table_img", ImageMime, MaxImageBytes, cancellationToken);

    public Task<string?> SaveTableVideoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "table_vid", VideoMime, MaxVideoBytes, cancellationToken);

    public Task<string?> SaveCafeLogoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "logo", ImageMime, MaxImageBytes, cancellationToken);

    public Task<string?> SaveCafeCoverAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "cover", ImageMime, MaxImageBytes, cancellationToken);

    public Task<string?> SaveReviewPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "review", ImageMime, MaxImageBytes, cancellationToken);

    public Task<string?> SaveCafeGalleryPhotoAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveAsync(cafeId, file, "gallery", ImageMime, MaxImageBytes, cancellationToken);

    public Task<string?> SaveMenuModel3dAsync(string cafeId, IFormFile file, CancellationToken cancellationToken = default)
        => SaveModel3dAsync(cafeId, file, cancellationToken);

    public async Task<string?> SaveMenuModel3dFromBytesAsync(
        string cafeId,
        byte[] glbBytes,
        CancellationToken cancellationToken = default)
    {
        if (glbBytes.Length == 0 || glbBytes.Length > MaxModel3dBytes) return null;

        var dir = Path.Combine(_env.ContentRootPath, "uploads", cafeId);
        Directory.CreateDirectory(dir);
        var savedName = $"menu_3d_ai_{Guid.NewGuid():N}.glb";
        var path = Path.Combine(dir, savedName);

        await File.WriteAllBytesAsync(path, glbBytes, cancellationToken);
        _logger.LogInformation("Saved AI 3D model for cafe {CafeId}", cafeId);
        return $"/uploads/{cafeId}/{savedName}";
    }

    private async Task<string?> SaveModel3dAsync(
        string cafeId,
        IFormFile file,
        CancellationToken cancellationToken)
    {
        if (file.Length == 0 || file.Length > MaxModel3dBytes) return null;

        var fileName = file.FileName.ToLowerInvariant();
        var isGlb = fileName.EndsWith(".glb", StringComparison.OrdinalIgnoreCase)
            || Model3dMime.Contains(file.ContentType);
        if (!isGlb) return null;

        await using var buffer = new MemoryStream();
        await file.CopyToAsync(buffer, cancellationToken);
        var bytes = buffer.ToArray();
        var hash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();

        var existing = await FindExistingByHashAsync(cafeId, hash, cancellationToken);
        if (existing is not null)
        {
            _logger.LogInformation("Dedup hit for 3D model (cafe {CafeId}); reusing existing file", cafeId);
            return existing;
        }

        var dir = Path.Combine(_env.ContentRootPath, "uploads", cafeId);
        Directory.CreateDirectory(dir);
        var savedName = $"menu_3d_{Guid.NewGuid():N}.glb";
        var path = Path.Combine(dir, savedName);

        await File.WriteAllBytesAsync(path, bytes, cancellationToken);

        var url = $"/uploads/{cafeId}/{savedName}";
        await RecordAsync(cafeId, hash, bytes.LongLength, file.ContentType, url, "menu_3d", file.FileName, cancellationToken);
        _logger.LogInformation("Saved 3D model media for cafe {CafeId}", cafeId);
        return url;
    }

    private async Task<string?> SaveAsync(
        string cafeId,
        IFormFile file,
        string prefix,
        HashSet<string> allowedMime,
        long maxBytes,
        CancellationToken cancellationToken)
    {
        if (file.Length == 0 || file.Length > maxBytes) return null;
        if (!allowedMime.Contains(file.ContentType)) return null;

        // Buffer once so we can hash the content and (if new) write it.
        await using var buffer = new MemoryStream();
        await file.CopyToAsync(buffer, cancellationToken);
        var bytes = buffer.ToArray();
        var hash = Convert.ToHexString(SHA256.HashData(bytes)).ToLowerInvariant();

        // Dedup: an identical file already stored for this scope is reused as-is.
        var existing = await FindExistingByHashAsync(cafeId, hash, cancellationToken);
        if (existing is not null)
        {
            _logger.LogInformation("Dedup hit for {Prefix} (cafe {CafeId}); reusing existing file", prefix, cafeId);
            return existing;
        }

        var ext = file.ContentType.ToLowerInvariant() switch
        {
            "image/png" => ".png",
            "image/webp" => ".webp",
            "video/webm" => ".webm",
            "video/quicktime" => ".mov",
            "video/mp4" => ".mp4",
            _ => ".jpg"
        };

        var dir = Path.Combine(_env.ContentRootPath, "uploads", cafeId);
        Directory.CreateDirectory(dir);
        var fileName = $"{prefix}_{Guid.NewGuid():N}{ext}";
        var path = Path.Combine(dir, fileName);

        await File.WriteAllBytesAsync(path, bytes, cancellationToken);

        var url = $"/uploads/{cafeId}/{fileName}";
        await RecordAsync(cafeId, hash, bytes.LongLength, file.ContentType, url, prefix, file.FileName, cancellationToken);
        _logger.LogInformation("Saved {Prefix} media for cafe {CafeId}", prefix, cafeId);
        return url;
    }

    // ─── Deduplication helpers ────────────────────────────────────────────────
    // MediaStorageService is a singleton; resolve a scoped DbContext per call.

    private async Task<string?> FindExistingByHashAsync(string? cafeId, string hash, CancellationToken ct)
    {
        try
        {
            await using var scope = _scopeFactory.CreateAsyncScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            return await db.MediaAssets.AsNoTracking()
                .Where(m => m.CafeId == cafeId && m.ContentHash == hash)
                .Select(m => m.Url)
                .FirstOrDefaultAsync(ct);
        }
        catch (Exception ex)
        {
            // Never let a dedup-lookup failure block an upload.
            _logger.LogWarning(ex, "Media dedup lookup failed; proceeding with a fresh upload");
            return null;
        }
    }

    private async Task RecordAsync(
        string? cafeId, string hash, long size, string contentType,
        string url, string kind, string? originalName, CancellationToken ct)
    {
        try
        {
            await using var scope = _scopeFactory.CreateAsyncScope();
            var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
            db.MediaAssets.Add(new MediaAsset
            {
                CafeId = cafeId,
                ContentHash = hash,
                SizeBytes = size,
                ContentType = contentType,
                Url = url,
                Kind = kind,
                OriginalFileName = originalName,
            });
            await db.SaveChangesAsync(ct);
        }
        catch (Exception ex)
        {
            // The file is already written; a missing dedup record only means a
            // future identical upload won't be de-duplicated. Don't fail the upload.
            _logger.LogWarning(ex, "Failed to record media asset for cafe {CafeId}", cafeId);
        }
    }
}
