using LightlessSync.Interop.Ipc;
using LightlessSync.FileCache;
using Microsoft.Extensions.Logging;
using Penumbra.Api.Enums;

namespace LightlessSync.Services.TextureCompression;

public sealed class TextureCompressionService
{
    private readonly ILogger<TextureCompressionService> _logger;
    private readonly IpcManager _ipcManager;
    private readonly FileCacheManager _fileCacheManager;

    public IReadOnlyList<TextureCompressionTarget> SelectableTargets => TextureCompressionCapabilities.SelectableTargets;
    public TextureCompressionTarget DefaultTarget => TextureCompressionCapabilities.DefaultTarget;

    public TextureCompressionService(
        ILogger<TextureCompressionService> logger,
        IpcManager ipcManager,
        FileCacheManager fileCacheManager)
    {
        _logger = logger;
        _ipcManager = ipcManager;
        _fileCacheManager = fileCacheManager;
    }

    public async Task ConvertTexturesAsync(
        IReadOnlyList<TextureCompressionRequest> requests,
        IProgress<TextureConversionProgress>? progress,
        CancellationToken token)
    {
        if (requests.Count == 0)
        {
            return;
        }

        var total = requests.Count;
        var completed = 0;

        foreach (var request in requests)
        {
            token.ThrowIfCancellationRequested();

            if (!TextureCompressionCapabilities.TryGetPenumbraTarget(request.Target, request.PrimaryFilePath, out var textureType))
            {
                _logger.LogWarning("Unsupported compression target {Target} requested.", request.Target);
                completed++;
                continue;
            }

            await RunPenumbraConversionAsync(request, textureType, total, completed, progress, token).ConfigureAwait(false);

            completed++;
        }
    }

    public bool IsTargetSelectable(TextureCompressionTarget target) => TextureCompressionCapabilities.IsSelectable(target);

    public TextureCompressionTarget NormalizeTarget(TextureCompressionTarget? desired) =>
        TextureCompressionCapabilities.Normalize(desired);

    private async Task RunPenumbraConversionAsync(
        TextureCompressionRequest request,
        TextureType targetType,
        int total,
        int completedBefore,
        IProgress<TextureConversionProgress>? progress,
        CancellationToken token)
    {
        var primaryPath = request.PrimaryFilePath;
        var displayJob = new TextureConversionJob(
            primaryPath,
            primaryPath,
            targetType,
            IncludeMipMaps: true,
            request.DuplicateFilePaths);

        var backupPath = CreateBackupCopy(primaryPath);
        var conversionJob = displayJob with { InputFile = backupPath };

        progress?.Report(new TextureConversionProgress(completedBefore, total, displayJob));

        try
        {
            WaitForAccess(primaryPath);
            await _ipcManager.Penumbra.ConvertTextureFiles(_logger, new[] { conversionJob }, null, token).ConfigureAwait(false);

            if (!IsValidConversionResult(displayJob.OutputFile))
            {
                throw new InvalidOperationException($"Penumbra conversion produced no output for {displayJob.OutputFile}.");
            }

            UpdateFileCache(displayJob);

            progress?.Report(new TextureConversionProgress(completedBefore + 1, total, displayJob));
        }
        catch (Exception ex)
        {
            RestoreFromBackup(backupPath, displayJob.OutputFile, displayJob.DuplicateTargets, ex);
            throw;
        }
        finally
        {
            CleanupBackup(backupPath);
        }
    }

    private void UpdateFileCache(TextureConversionJob job)
    {
        var paths = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            job.OutputFile
        };

        if (job.DuplicateTargets is { Count: > 0 })
        {
            foreach (var duplicate in job.DuplicateTargets)
            {
                paths.Add(duplicate);
            }
        }

        if (paths.Count == 0)
        {
            return;
        }

        var cacheEntries = _fileCacheManager.GetFileCachesByPaths(paths.ToArray());
        foreach (var path in paths)
        {
            if (!cacheEntries.TryGetValue(path, out var entry) || entry is null)
            {
                entry = _fileCacheManager.CreateFileEntry(path);
                if (entry is null)
                {
                    _logger.LogWarning("Unable to locate cache entry for {Path}; skipping hash refresh", path);
                    continue;
                }
            }

            try
            {
                _fileCacheManager.UpdateHashedFile(entry);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Failed to refresh file cache entry for {Path}", path);
            }
        }
    }

    private static readonly string WorkingDirectory =
        Path.Combine(Path.GetTempPath(), "LightlessSync.TextureCompression");

    private static string CreateBackupCopy(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($"Cannot back up missing texture file {filePath}.", filePath);
        }

        Directory.CreateDirectory(WorkingDirectory);

        var extension = Path.GetExtension(filePath);
        if (string.IsNullOrEmpty(extension))
        {
            extension = ".tmp";
        }

        var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
        var backupName = $"{fileNameWithoutExtension}.backup.{Guid.NewGuid():N}{extension}";
        var backupPath = Path.Combine(WorkingDirectory, backupName);

        WaitForAccess(filePath);

        File.Copy(filePath, backupPath, overwrite: false);

        return backupPath;
    }

    private const int MaxAccessRetries = 10;
    private static readonly TimeSpan AccessRetryDelay = TimeSpan.FromMilliseconds(200);

    private static void WaitForAccess(string filePath)
    {
        if (!File.Exists(filePath))
        {
            return;
        }

        try
        {
            File.SetAttributes(filePath, FileAttributes.Normal);
        }
        catch
        {
            // ignore attribute changes here
        }

        Exception? lastException = null;
        for (var attempt = 0; attempt < MaxAccessRetries; attempt++)
        {
            try
            {
                using var stream = new FileStream(
                    filePath,
                    FileMode.Open,
                    FileAccess.Read,
                    FileShare.None);
                return;
            }
            catch (IOException ex) when (IsSharingViolation(ex))
            {
                lastException = ex;
            }

            Thread.Sleep(AccessRetryDelay);
        }

        if (lastException != null)
        {
            throw lastException;
        }
    }

    private static bool IsSharingViolation(IOException ex) =>
        ex.HResult == unchecked((int)0x80070020);

    private void RestoreFromBackup(
        string backupPath,
        string destinationPath,
        IReadOnlyList<string>? duplicateTargets,
        Exception reason)
    {
        if (string.IsNullOrEmpty(backupPath))
        {
            _logger.LogWarning(reason, "Conversion failed for {File}, but no backup was available to restore.", destinationPath);
            return;
        }

        if (!File.Exists(backupPath))
        {
            _logger.LogWarning(reason, "Conversion failed for {File}, but backup path {Backup} no longer exists.", destinationPath, backupPath);
            return;
        }

        try
        {
            TryReplaceFile(backupPath, destinationPath);
        }
        catch (Exception restoreEx)
        {
            _logger.LogError(restoreEx, "Failed to restore texture {File} after conversion failure.", destinationPath);
            return;
        }

        if (duplicateTargets is { Count: > 0 })
        {
            foreach (var duplicate in duplicateTargets)
            {
                if (string.Equals(destinationPath, duplicate, StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                try
                {
                    File.Copy(destinationPath, duplicate, overwrite: true);
                }
                catch (Exception duplicateEx)
                {
                    _logger.LogDebug(duplicateEx, "Failed to restore duplicate {Duplicate} after conversion failure.", duplicate);
                }
            }
        }

        _logger.LogWarning(reason, "Restored original texture {File} after conversion failure.", destinationPath);
    }

    private static void TryReplaceFile(string sourcePath, string destinationPath)
    {
        WaitForAccess(destinationPath);

        var destinationDirectory = Path.GetDirectoryName(destinationPath);
        if (!string.IsNullOrEmpty(destinationDirectory))
        {
            Directory.CreateDirectory(destinationDirectory);
        }

        File.Copy(sourcePath, destinationPath, overwrite: true);
    }

    private static void CleanupBackup(string backupPath)
    {
        if (string.IsNullOrEmpty(backupPath))
        {
            return;
        }

        try
        {
            if (File.Exists(backupPath))
            {
                File.Delete(backupPath);
            }
        }
        catch
        {
            // avoid killing successful conversions on cleanup failure
        }
    }

    private static bool IsValidConversionResult(string path)
    {
        try
        {
            var fileInfo = new FileInfo(path);
            return fileInfo.Exists && fileInfo.Length > 0;
        }
        catch
        {
            return false;
        }
    }
}
