using System.Diagnostics;
using System.Security.Cryptography;
using YMhut.Box.Core.App;
using YMhut.Box.Core.Downloads;
using YMhut.Box.Core.Logging;

namespace YMhut.Box.WinUI.Services;

public sealed class DownloadManagerService : IDownloadManagerService, IDisposable
{
    private readonly AppPaths _paths;
    private readonly IDownloadQueueStore _store;
    private readonly IDownloadHostProcessService _host;
    private readonly ILogService? _logService;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly List<DownloadItem> _items = [];
    private DownloadSettings _settings;
    private bool _initialized;
    private bool _disposed;

    public DownloadManagerService(
        AppPaths paths,
        IDownloadQueueStore store,
        IDownloadHostProcessService host,
        ILogService? logService = null)
    {
        _paths = paths;
        _store = store;
        _host = host;
        _logService = logService;
        _settings = new DownloadSettings(Path.Combine(paths.Data, "Downloads"), 5);
        _host.ProgressChanged += Host_ProgressChanged;
    }

    public event EventHandler? ItemsChanged;

    public IReadOnlyList<DownloadItem> Items
    {
        get
        {
            lock (_items)
            {
                return _items.ToArray();
            }
        }
    }

    public DownloadSettings Settings => _settings;

    public async Task InitializeAsync(CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (_initialized)
            {
                return;
            }

            _settings = await _store.LoadSettingsAsync(cancellationToken).ConfigureAwait(false);
            Directory.CreateDirectory(_settings.DefaultDirectory);
            var loaded = await _store.LoadAsync(cancellationToken).ConfigureAwait(false);
            lock (_items)
            {
                _items.Clear();
                _items.AddRange(loaded.Select(NormalizeLoadedItem));
            }

            _initialized = true;
        }
        finally
        {
            _gate.Release();
        }

        RaiseChanged();
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task<DownloadItem> EnqueueAsync(
        DownloadSource source,
        string? targetDirectory = null,
        string? installCommand = null,
        string? installArguments = null,
        CancellationToken cancellationToken = default)
    {
        return await EnqueueAsync(
                source,
                new DownloadOptions(
                    TargetDirectory: targetDirectory,
                    InstallCommand: installCommand,
                    InstallArguments: installArguments,
                    IsInstaller: !string.IsNullOrWhiteSpace(installCommand)),
                cancellationToken)
            .ConfigureAwait(false);
    }

    public async Task<DownloadItem> EnqueueAsync(
        DownloadSource source,
        DownloadOptions options,
        CancellationToken cancellationToken = default)
    {
        await InitializeAsync(cancellationToken).ConfigureAwait(false);
        var targetPath = ResolveTargetPath(source, options);
        Directory.CreateDirectory(Path.GetDirectoryName(targetPath) ?? _settings.DefaultDirectory);

        var fileName = SanitizeFileName(Path.GetFileName(targetPath));
        var normalizedSource = source with
        {
            FileName = string.IsNullOrWhiteSpace(fileName) ? SanitizeFileName(source.FileName) : fileName
        };
        var item = DownloadItem.Create(
            normalizedSource,
            targetPath,
            options.InstallCommand,
            options.InstallArguments,
            options.IsInstaller,
            options.DeleteAfterInstall);

        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            lock (_items)
            {
                _items.Insert(0, item);
            }

            await PersistAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }

        RaiseChanged();
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
        return item;
    }

    public async Task UpdateSettingsAsync(DownloadSettings settings, CancellationToken cancellationToken = default)
    {
        await InitializeAsync(cancellationToken).ConfigureAwait(false);
        var next = settings with
        {
            DefaultDirectory = string.IsNullOrWhiteSpace(settings.DefaultDirectory)
                ? _settings.DefaultDirectory
                : settings.DefaultDirectory,
            MaxConcurrentDownloads = settings.EffectiveMaxConcurrentDownloads
        };
        Directory.CreateDirectory(next.DefaultDirectory);
        _settings = next;
        await _store.SaveSettingsAsync(next, cancellationToken).ConfigureAwait(false);
        RaiseChanged();
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task StartAsync(string id, CancellationToken cancellationToken = default)
    {
        await InitializeAsync(cancellationToken).ConfigureAwait(false);
        var item = Find(id);
        if (item is null)
        {
            return;
        }

        if (item.State is DownloadState.Running)
        {
            return;
        }

        await UpdateAsync(item.WithProgress(DownloadState.Queued, item.ReceivedBytes, item.TotalBytes, 0, null), cancellationToken)
            .ConfigureAwait(false);
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task PauseAsync(string id, CancellationToken cancellationToken = default)
    {
        var item = Find(id);
        if (item is null)
        {
            return;
        }

        if (item.State == DownloadState.Running)
        {
            await _host.PauseAsync(id, cancellationToken).ConfigureAwait(false);
        }

        await UpdateAsync(item.WithProgress(DownloadState.Paused, item.ReceivedBytes, item.TotalBytes, 0), cancellationToken)
            .ConfigureAwait(false);
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task ResumeAsync(string id, CancellationToken cancellationToken = default)
    {
        var item = Find(id);
        if (item is null)
        {
            return;
        }

        await UpdateAsync(item.WithProgress(DownloadState.Queued, item.ReceivedBytes, item.TotalBytes, 0), cancellationToken)
            .ConfigureAwait(false);
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task CancelAsync(string id, CancellationToken cancellationToken = default)
    {
        var item = Find(id);
        if (item is null)
        {
            return;
        }

        if (item.State == DownloadState.Running)
        {
            await _host.CancelAsync(id, cancellationToken).ConfigureAwait(false);
        }
        else
        {
            TryDeleteFile(item.EffectivePartialPath);
        }

        await UpdateAsync(item.WithProgress(DownloadState.Canceled, item.ReceivedBytes, item.TotalBytes, 0), cancellationToken)
            .ConfigureAwait(false);
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task RetryAsync(string id, CancellationToken cancellationToken = default)
    {
        var item = Find(id);
        if (item is null)
        {
            return;
        }

        TryDeleteFile(item.EffectivePartialPath);
        var reset = item with
        {
            ETag = string.Empty,
            LastModified = string.Empty,
            AcceptRanges = string.Empty,
            ContentLength = null,
            FinalUrl = string.Empty,
            ResumeSupported = false
        };
        await UpdateAsync(reset.WithProgress(DownloadState.Queued, 0, null, 0, null), cancellationToken)
            .ConfigureAwait(false);
        await PumpQueueAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task ClearCompletedAsync(CancellationToken cancellationToken = default)
    {
        await InitializeAsync(cancellationToken).ConfigureAwait(false);
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            lock (_items)
            {
                _items.RemoveAll(item => item.State is DownloadState.Completed or DownloadState.Canceled);
            }

            await PersistAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }

        RaiseChanged();
    }

    public async Task MarkInstallLaunchedAsync(string id, bool cleanupCompleted = false, CancellationToken cancellationToken = default)
    {
        var item = Find(id);
        if (item is null)
        {
            return;
        }

        await UpdateAsync(item.WithInstallState(true, cleanupCompleted), cancellationToken).ConfigureAwait(false);
    }

    public async Task TryCleanupAfterInstallAsync(string id, CancellationToken cancellationToken = default)
    {
        var item = Find(id);
        if (item is null ||
            !item.DeleteAfterInstall ||
            item.InstallCleanupCompleted ||
            item.State != DownloadState.Completed ||
            string.IsNullOrWhiteSpace(item.TargetPath) ||
            !File.Exists(item.TargetPath))
        {
            return;
        }

        try
        {
            File.Delete(item.TargetPath);
            await UpdateAsync(item.WithInstallState(item.InstallLaunched, cleanupCompleted: true), cancellationToken)
                .ConfigureAwait(false);
            await WriteLogAsync("Information", "download", "Installer package cleaned", item.TargetPath, cancellationToken)
                .ConfigureAwait(false);
        }
        catch (Exception exception)
        {
            await WriteLogAsync("Warning", "download", "Installer cleanup failed", exception.Message, cancellationToken)
                .ConfigureAwait(false);
        }
    }

    public void Dispose()
    {
        if (_disposed)
        {
            return;
        }

        _disposed = true;
        _host.ProgressChanged -= Host_ProgressChanged;
        if (_host is IDisposable disposable)
        {
            disposable.Dispose();
        }

        _gate.Dispose();
    }

    public static Process? LaunchInstaller(DownloadItem item)
    {
        if (string.IsNullOrWhiteSpace(item.TargetPath) || !File.Exists(item.TargetPath))
        {
            return null;
        }

        var command = item.InstallCommand;
        var fileName = string.IsNullOrWhiteSpace(command) || string.Equals(command, "installer", StringComparison.OrdinalIgnoreCase)
            ? item.TargetPath
            : command;
        var arguments = string.IsNullOrWhiteSpace(command) || string.Equals(command, "installer", StringComparison.OrdinalIgnoreCase)
            ? item.InstallArguments ?? string.Empty
            : BuildArguments(item);

        return Process.Start(new ProcessStartInfo
        {
            FileName = fileName,
            Arguments = arguments,
            WorkingDirectory = Path.GetDirectoryName(item.TargetPath) ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
            UseShellExecute = true
        });
    }

    private async Task PumpQueueAsync(CancellationToken cancellationToken)
    {
        await InitializeAsync(cancellationToken).ConfigureAwait(false);

        while (true)
        {
            DownloadItem? next;
            await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
            try
            {
                lock (_items)
                {
                    var running = _items.Count(item => item.State == DownloadState.Running);
                    if (running >= _settings.EffectiveMaxConcurrentDownloads)
                    {
                        return;
                    }

                    var index = _items.FindLastIndex(item => item.State == DownloadState.Queued);
                    if (index < 0)
                    {
                        return;
                    }

                    var current = _items[index];
                    var partialBytes = FileLength(current.EffectivePartialPath);
                    var startBytes = partialBytes > 0 ? partialBytes : 0;
                    var startTotal = partialBytes > 0 ? current.TotalBytes ?? current.ContentLength : current.TotalBytes;
                    next = current.WithResumeMetadata(
                            contentLength: current.ContentLength,
                            resumeSupported: partialBytes > 0 && current.ResumeSupported)
                        .WithProgress(
                        DownloadState.Running,
                        startBytes,
                        startTotal,
                        0,
                        null);
                    _items[index] = next;
                }

                await PersistAsync(cancellationToken).ConfigureAwait(false);
            }
            finally
            {
                _gate.Release();
            }

            RaiseChanged();
            try
            {
                if (FileLength(next.EffectivePartialPath) > 0)
                {
                    await _host.ResumeAsync(next, cancellationToken).ConfigureAwait(false);
                }
                else
                {
                    await _host.StartAsync(next, cancellationToken).ConfigureAwait(false);
                }

                await WriteLogAsync("Information", "download", "Download started", next.Source.DisplayName, cancellationToken)
                    .ConfigureAwait(false);
            }
            catch (Exception exception)
            {
                await UpdateAsync(next.WithProgress(DownloadState.Failed, next.ReceivedBytes, next.TotalBytes, 0, exception.Message), cancellationToken)
                    .ConfigureAwait(false);
            }
        }
    }

    private void Host_ProgressChanged(object? sender, DownloadProgressSnapshot progress)
    {
        _ = Task.Run(async () =>
        {
            var item = Find(progress.Id);
            if (item is null)
            {
                return;
            }

            var resumeSupported = IsResumeSupported(progress);
            var next = item
                .WithResumeMetadata(
                    progress.ETag,
                    progress.LastModified,
                    progress.AcceptRanges,
                    progress.ContentLength,
                    progress.FinalUrl,
                    resumeSupported)
                .WithProgress(progress.State, progress.ReceivedBytes, progress.TotalBytes, progress.BytesPerSecond, progress.Error);
            if (progress.State == DownloadState.Completed)
            {
                next = await VerifyCompletedAsync(next).ConfigureAwait(false);
            }

            await UpdateAsync(next, CancellationToken.None).ConfigureAwait(false);

            if (progress.State is DownloadState.Completed or DownloadState.Failed or DownloadState.Canceled or DownloadState.Paused)
            {
                await PumpQueueAsync(CancellationToken.None).ConfigureAwait(false);
            }
        });
    }

    private async Task<DownloadItem> VerifyCompletedAsync(DownloadItem item)
    {
        if (string.IsNullOrWhiteSpace(item.Source.Sha256))
        {
            await WriteLogAsync("Information", "download", "Download completed", item.TargetPath, CancellationToken.None)
                .ConfigureAwait(false);
            return item;
        }

        try
        {
            await using var stream = File.OpenRead(item.TargetPath);
            var hash = Convert.ToHexString(await SHA256.HashDataAsync(stream).ConfigureAwait(false)).ToLowerInvariant();
            if (!string.Equals(hash, item.Source.Sha256, StringComparison.OrdinalIgnoreCase))
            {
                return item.WithProgress(
                    DownloadState.Failed,
                    item.ReceivedBytes,
                    item.TotalBytes,
                    0,
                    $"SHA256 mismatch. Expected {item.Source.Sha256}, got {hash}.");
            }
        }
        catch (Exception exception)
        {
            return item.WithProgress(DownloadState.Failed, item.ReceivedBytes, item.TotalBytes, 0, exception.Message);
        }

        await WriteLogAsync("Information", "download", "Download completed and verified", item.TargetPath, CancellationToken.None)
            .ConfigureAwait(false);
        return item;
    }

    private DownloadItem? Find(string id)
    {
        lock (_items)
        {
            return _items.FirstOrDefault(item => string.Equals(item.Id, id, StringComparison.OrdinalIgnoreCase));
        }
    }

    private async Task UpdateAsync(DownloadItem next, CancellationToken cancellationToken)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            lock (_items)
            {
                var index = _items.FindIndex(item => string.Equals(item.Id, next.Id, StringComparison.OrdinalIgnoreCase));
                if (index >= 0)
                {
                    _items[index] = next;
                }
            }

            await PersistAsync(cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }

        RaiseChanged();
    }

    private async Task PersistAsync(CancellationToken cancellationToken)
    {
        DownloadItem[] snapshot;
        lock (_items)
        {
            snapshot = _items.ToArray();
        }

        await _store.SaveAsync(snapshot, cancellationToken).ConfigureAwait(false);
    }

    private void RaiseChanged()
    {
        ItemsChanged?.Invoke(this, EventArgs.Empty);
    }

    private async Task WriteLogAsync(string level, string category, string message, string? detail, CancellationToken cancellationToken)
    {
        if (_logService is not null)
        {
            await _logService.WriteAsync(level, category, message, detail, cancellationToken).ConfigureAwait(false);
        }
    }

    private string ResolveTargetPath(DownloadSource source, DownloadOptions options)
    {
        if (!string.IsNullOrWhiteSpace(options.TargetPath))
        {
            return NextAvailablePath(options.TargetPath);
        }

        var directory = string.IsNullOrWhiteSpace(options.TargetDirectory)
            ? _settings.DefaultDirectory
            : options.TargetDirectory;
        var fileName = SanitizeFileName(string.IsNullOrWhiteSpace(source.FileName) ? "download.bin" : source.FileName);
        return NextAvailablePath(Path.Combine(directory, fileName));
    }

    private static string BuildArguments(DownloadItem item)
    {
        var args = item.InstallArguments ?? string.Empty;
        return args.Contains("{file}", StringComparison.OrdinalIgnoreCase)
            ? args.Replace("{file}", Quote(item.TargetPath), StringComparison.OrdinalIgnoreCase)
            : $"{Quote(item.TargetPath)} {args}".Trim();
    }

    private static string Quote(string value)
    {
        return "\"" + value.Replace("\"", "\\\"", StringComparison.Ordinal) + "\"";
    }

    private static string SanitizeFileName(string fileName)
    {
        var invalid = Path.GetInvalidFileNameChars();
        var clean = new string(fileName.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
        return string.IsNullOrWhiteSpace(clean) ? "download.bin" : clean;
    }

    private static string NextAvailablePath(string path)
    {
        if (!File.Exists(path) && !File.Exists(path + ".partial"))
        {
            return path;
        }

        var directory = Path.GetDirectoryName(path) ?? Environment.CurrentDirectory;
        var name = Path.GetFileNameWithoutExtension(path);
        var extension = Path.GetExtension(path);
        for (var index = 2; index < 10_000; index++)
        {
            var candidate = Path.Combine(directory, $"{name} ({index}){extension}");
            if (!File.Exists(candidate) && !File.Exists(candidate + ".partial"))
            {
                return candidate;
            }
        }

        return Path.Combine(directory, $"{name}-{Guid.NewGuid():N}{extension}");
    }

    private static DownloadItem NormalizeLoadedItem(DownloadItem item)
    {
        var withPartialPath = string.IsNullOrWhiteSpace(item.PartialPath)
            ? item with { PartialPath = item.TargetPath + ".partial" }
            : item;
        var partialBytes = FileLength(withPartialPath.EffectivePartialPath);
        return withPartialPath.State == DownloadState.Running
            ? withPartialPath.WithProgress(DownloadState.Queued, partialBytes, withPartialPath.TotalBytes ?? withPartialPath.ContentLength, 0)
            : withPartialPath;
    }

    private static bool IsResumeSupported(DownloadProgressSnapshot progress)
    {
        return progress.AcceptRanges.Contains("bytes", StringComparison.OrdinalIgnoreCase) ||
            !string.IsNullOrWhiteSpace(progress.ETag) ||
            !string.IsNullOrWhiteSpace(progress.LastModified);
    }

    private static long FileLength(string path)
    {
        try
        {
            return File.Exists(path) ? new FileInfo(path).Length : 0;
        }
        catch
        {
            return 0;
        }
    }

    private static void TryDeleteFile(string path)
    {
        try
        {
            if (File.Exists(path))
            {
                File.Delete(path);
            }
        }
        catch
        {
        }
    }
}
