using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration;
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using System.Globalization;

namespace LightlessSync.Services.ModelDecimation;

public sealed class ModelDecimationService
{
    private const int MaxConcurrentJobs = 1;
    private const double MinTargetRatio = 0.01;
    private const double MaxTargetRatio = 0.99;

    private readonly ILogger<ModelDecimationService> _logger;
    private readonly LightlessConfigService _configService;
    private readonly FileCacheManager _fileCacheManager;
    private readonly PlayerPerformanceConfigService _performanceConfigService;
    private readonly XivDataStorageService _xivDataStorageService;
    private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);

    private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
    private readonly ConcurrentDictionary<string, string> _decimatedPaths = new(StringComparer.OrdinalIgnoreCase);
    private readonly ConcurrentDictionary<string, byte> _failedHashes = new(StringComparer.OrdinalIgnoreCase);

    public ModelDecimationService(
        ILogger<ModelDecimationService> logger,
        LightlessConfigService configService,
        FileCacheManager fileCacheManager,
        PlayerPerformanceConfigService performanceConfigService,
        XivDataStorageService xivDataStorageService)
    {
        _logger = logger;
        _configService = configService;
        _fileCacheManager = fileCacheManager;
        _performanceConfigService = performanceConfigService;
        _xivDataStorageService = xivDataStorageService;
    }

    public void ScheduleDecimation(string hash, string filePath, string? gamePath = null)
    {
        if (!ShouldScheduleDecimation(hash, filePath, gamePath))
        {
            return;
        }

        if (_decimatedPaths.ContainsKey(hash) || _failedHashes.ContainsKey(hash) || _activeJobs.ContainsKey(hash))
        {
            return;
        }

        _logger.LogInformation("Queued model decimation for {Hash}", hash);

        _activeJobs[hash] = Task.Run(async () =>
        {
            await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
            try
            {
                await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                _failedHashes[hash] = 1;
                _logger.LogWarning(ex, "Model decimation failed for {Hash}", hash);
            }
            finally
            {
                _decimationSemaphore.Release();
                _activeJobs.TryRemove(hash, out _);
            }
        }, CancellationToken.None);
    }

    public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
        => IsDecimationEnabled()
            && filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
            && IsDecimationAllowed(gamePath)
            && !ShouldSkipByTriangleCache(hash);

    public string GetPreferredPath(string hash, string originalPath)
    {
        if (!IsDecimationEnabled())
        {
            return originalPath;
        }

        if (_decimatedPaths.TryGetValue(hash, out var existing) && File.Exists(existing))
        {
            return existing;
        }

        var resolved = GetExistingDecimatedPath(hash);
        if (!string.IsNullOrEmpty(resolved))
        {
            _decimatedPaths[hash] = resolved;
            return resolved;
        }

        return originalPath;
    }

    public Task WaitForPendingJobsAsync(IEnumerable<string>? hashes, CancellationToken token)
    {
        if (hashes is null)
        {
            return Task.CompletedTask;
        }

        var pending = new List<Task>();
        var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        foreach (var hash in hashes)
        {
            if (string.IsNullOrEmpty(hash) || !seen.Add(hash))
            {
                continue;
            }

            if (_activeJobs.TryGetValue(hash, out var job))
            {
                pending.Add(job);
            }
        }

        if (pending.Count == 0)
        {
            return Task.CompletedTask;
        }

        return Task.WhenAll(pending).WaitAsync(token);
    }

    private Task DecimateInternalAsync(string hash, string sourcePath)
    {
        if (!File.Exists(sourcePath))
        {
            _failedHashes[hash] = 1;
            _logger.LogWarning("Cannot decimate model {Hash}; source path missing: {Path}", hash, sourcePath);
            return Task.CompletedTask;
        }

        if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio))
        {
            _logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
            return Task.CompletedTask;
        }

        _logger.LogInformation("Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##})", hash, triangleThreshold, targetRatio);

        var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
        if (File.Exists(destination))
        {
            RegisterDecimatedModel(hash, sourcePath, destination);
            return Task.CompletedTask;
        }

        if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, _logger))
        {
            _failedHashes[hash] = 1;
            _logger.LogInformation("Model decimation skipped for {Hash}", hash);
            return Task.CompletedTask;
        }

        RegisterDecimatedModel(hash, sourcePath, destination);
        _logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination);
        return Task.CompletedTask;
    }

    private void RegisterDecimatedModel(string hash, string sourcePath, string destination)
    {
        _decimatedPaths[hash] = destination;

        var performanceConfig = _performanceConfigService.Current;
        if (performanceConfig.KeepOriginalModelFiles)
        {
            return;
        }

        if (string.Equals(sourcePath, destination, StringComparison.OrdinalIgnoreCase))
        {
            return;
        }

        if (!TryReplaceCacheEntryWithDecimated(hash, sourcePath, destination))
        {
            return;
        }

        TryDelete(sourcePath);
    }

    private bool TryReplaceCacheEntryWithDecimated(string hash, string sourcePath, string destination)
    {
        try
        {
            var cacheEntry = _fileCacheManager.GetFileCacheByHash(hash);
            if (cacheEntry is null || !cacheEntry.IsCacheEntry)
            {
                return File.Exists(sourcePath) ? false : true;
            }

            var cacheFolder = _configService.Current.CacheFolder;
            if (string.IsNullOrEmpty(cacheFolder))
            {
                return false;
            }

            if (!destination.StartsWith(cacheFolder, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            var info = new FileInfo(destination);
            if (!info.Exists)
            {
                return false;
            }

            var relative = Path.GetRelativePath(cacheFolder, destination)
                .Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
            var sanitizedRelative = relative.TrimStart(Path.DirectorySeparatorChar);
            var prefixed = Path.Combine(FileCacheManager.CachePrefix, sanitizedRelative);

            var replacement = new FileCacheEntity(
                hash,
                prefixed,
                info.LastWriteTimeUtc.Ticks.ToString(CultureInfo.InvariantCulture),
                info.Length,
                cacheEntry.CompressedSize);
            replacement.SetResolvedFilePath(destination);

            if (!string.Equals(cacheEntry.PrefixedFilePath, prefixed, StringComparison.OrdinalIgnoreCase))
            {
                _fileCacheManager.RemoveHashedFile(cacheEntry.Hash, cacheEntry.PrefixedFilePath, removeDerivedFiles: false);
            }

            _fileCacheManager.UpdateHashedFile(replacement, computeProperties: false);
            _fileCacheManager.WriteOutFullCsv();

            _logger.LogTrace("Replaced cache entry for model {Hash} to decimated path {Path}", hash, destination);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogTrace(ex, "Failed to replace cache entry for model {Hash}", hash);
            return false;
        }
    }

    private bool IsDecimationEnabled()
        => _performanceConfigService.Current.EnableModelDecimation;

    private bool ShouldSkipByTriangleCache(string hash)
    {
        if (string.IsNullOrEmpty(hash))
        {
            return false;
        }

        if (!_xivDataStorageService.Current.TriangleDictionary.TryGetValue(hash, out var cachedTris) || cachedTris <= 0)
        {
            return false;
        }

        var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
        return threshold > 0 && cachedTris < threshold;
    }

    private bool IsDecimationAllowed(string? gamePath)
    {
        if (string.IsNullOrWhiteSpace(gamePath))
        {
            return true;
        }

        var normalized = NormalizeGamePath(gamePath);
        if (normalized.Contains("/hair/", StringComparison.Ordinal))
        {
            return false;
        }

        if (normalized.Contains("/chara/equipment/", StringComparison.Ordinal))
        {
            return _performanceConfigService.Current.ModelDecimationAllowClothing;
        }

        if (normalized.Contains("/chara/accessory/", StringComparison.Ordinal))
        {
            return _performanceConfigService.Current.ModelDecimationAllowAccessories;
        }

        if (normalized.Contains("/chara/human/", StringComparison.Ordinal))
        {
            if (normalized.Contains("/body/", StringComparison.Ordinal))
            {
                return _performanceConfigService.Current.ModelDecimationAllowBody;
            }

            if (normalized.Contains("/face/", StringComparison.Ordinal) || normalized.Contains("/head/", StringComparison.Ordinal))
            {
                return _performanceConfigService.Current.ModelDecimationAllowFaceHead;
            }

            if (normalized.Contains("/tail/", StringComparison.Ordinal))
            {
                return _performanceConfigService.Current.ModelDecimationAllowTail;
            }
        }

        return true;
    }

    private static string NormalizeGamePath(string path)
        => path.Replace('\\', '/').ToLowerInvariant();

    private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio)
    {
        triangleThreshold = 15_000;
        targetRatio = 0.8;

        var config = _performanceConfigService.Current;
        if (!config.EnableModelDecimation)
        {
            return false;
        }

        triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
        targetRatio = config.ModelDecimationTargetRatio;
        if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio))
        {
            return false;
        }

        targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio);
        return true;
    }

    private string? GetExistingDecimatedPath(string hash)
    {
        var candidate = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
        return File.Exists(candidate) ? candidate : null;
    }

    private string GetDecimatedDirectory()
    {
        var directory = Path.Combine(_configService.Current.CacheFolder, "decimated");
        if (!Directory.Exists(directory))
        {
            try
            {
                Directory.CreateDirectory(directory);
            }
            catch (Exception ex)
            {
                _logger.LogTrace(ex, "Failed to create decimated directory {Directory}", directory);
            }
        }

        return directory;
    }

    private static void TryDelete(string? path)
    {
        if (string.IsNullOrEmpty(path))
        {
            return;
        }

        try
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }
        catch
        {
            // ignored
        }
    }
}
