using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using PhotoboothUploader.Models;

namespace PhotoboothUploader.Services;

public sealed class UploadService
{
    private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(20);
    private static readonly TimeSpan RetryBaseDelay = TimeSpan.FromSeconds(2);
    private const int MaxRetries = 2;
    private readonly Channel<string> _queue = Channel.CreateUnbounded<string>();
    private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
    private string _userAgent = "FotospielPhotoboothUploader";
    private CancellationTokenSource? _cts;
    private readonly List<Task> _workers = new();

    public void Configure(string userAgent)
    {
        if (!string.IsNullOrWhiteSpace(userAgent))
        {
            _userAgent = userAgent;
        }
    }

    public void Start(
        PhotoboothSettings settings,
        Action<string> onQueued,
        Action<string> onUploading,
        Action<string> onSuccess,
        Action<string, string> onFailure)
    {
        Stop();

        _cts = new CancellationTokenSource();
        var workerCount = GetWorkerCount(settings);
        for (var i = 0; i < workerCount; i++)
        {
            _workers.Add(Task.Run(() => WorkerAsync(settings, onQueued, onUploading, onSuccess, onFailure, _cts.Token)));
        }
    }

    public void Stop()
    {
        _cts?.Cancel();
        _cts = null;
        _pending.Clear();
        _workers.Clear();
    }

    public void Enqueue(string path, Action<string> onQueued)
    {
        if (!_pending.TryAdd(path, 0))
        {
            return;
        }

        _queue.Writer.TryWrite(path);
        onQueued(path);
    }

    private async Task WorkerAsync(
        PhotoboothSettings settings,
        Action<string> onQueued,
        Action<string> onUploading,
        Action<string> onSuccess,
        Action<string, string> onFailure,
        CancellationToken token)
    {
        if (string.IsNullOrWhiteSpace(settings.UploadUrl))
        {
            return;
        }

        using var client = new HttpClient();
        client.Timeout = DefaultTimeout;
        client.DefaultRequestHeaders.UserAgent.ParseAdd(_userAgent);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        while (await _queue.Reader.WaitToReadAsync(token))
        {
            while (_queue.Reader.TryRead(out var path))
            {
                try
                {
                    onUploading(path);
                    var error = await UploadWithRetryAsync(client, settings, path, token);
                    if (error is null)
                    {
                        onSuccess(path);
                    }
                    else
                    {
                        onFailure(path, error);
                    }
                }
                catch (OperationCanceledException)
                {
                    return;
                }
                finally
                {
                    _pending.TryRemove(path, out _);
                    if (settings.UploadDelayMs > 0)
                    {
                        await Task.Delay(settings.UploadDelayMs, token);
                    }
                }
            }
        }
    }

    private static async Task<string?> UploadWithRetryAsync(
        HttpClient client,
        PhotoboothSettings settings,
        string path,
        CancellationToken token)
    {
        for (var attempt = 0; attempt <= MaxRetries; attempt++)
        {
            var attemptError = await UploadOnceAsync(client, settings, path, token);
            if (attemptError.Success)
            {
                return null;
            }

            if (!attemptError.Retryable || attempt >= MaxRetries)
            {
                return attemptError.Error ?? "Upload fehlgeschlagen.";
            }

            await Task.Delay(GetRetryDelay(attempt), token);
        }

        return "Upload fehlgeschlagen.";
    }

    private static async Task<UploadAttempt> UploadOnceAsync(
        HttpClient client,
        PhotoboothSettings settings,
        string path,
        CancellationToken token)
    {
        var readyError = await WaitForFileReadyAsync(path, token);
        if (readyError is not null)
        {
            return UploadAttempt.Fail(readyError, retryable: false);
        }

        if (!File.Exists(path))
        {
            return UploadAttempt.Fail("Datei nicht gefunden.", retryable: false);
        }

        using var content = new MultipartFormDataContent();

        if (!string.IsNullOrWhiteSpace(settings.Username))
        {
            content.Add(new StringContent(settings.Username), "username");
        }

        if (!string.IsNullOrWhiteSpace(settings.Password))
        {
            content.Add(new StringContent(settings.Password), "password");
        }

        if (!string.IsNullOrWhiteSpace(settings.ResponseFormat))
        {
            content.Add(new StringContent(settings.ResponseFormat), "format");
        }

        var stream = File.OpenRead(path);
        var fileContent = new StreamContent(stream);
        fileContent.Headers.ContentType = new MediaTypeHeaderValue(ResolveContentType(path));
        content.Add(fileContent, "media", Path.GetFileName(path));

        try
        {
            var response = await client.PostAsync(settings.UploadUrl, content, token);

            if (response.IsSuccessStatusCode)
            {
                return UploadAttempt.Ok();
            }

            var body = await ReadResponseBodyAsync(response, token);
            var status = $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
            var message = string.IsNullOrWhiteSpace(body) ? status : $"{status} – {body}";
            return UploadAttempt.Fail(message, IsRetryableStatus(response.StatusCode));
        }
        catch (TaskCanceledException) when (!token.IsCancellationRequested)
        {
            return UploadAttempt.Fail("Zeitüberschreitung beim Upload.", retryable: true);
        }
        catch (HttpRequestException)
        {
            return UploadAttempt.Fail("Netzwerkfehler beim Upload.", retryable: true);
        }
        catch (IOException)
        {
            return UploadAttempt.Fail("Datei konnte nicht gelesen werden.", retryable: false);
        }
    }

    private static async Task<string?> WaitForFileReadyAsync(string path, CancellationToken token)
    {
        var lastSize = -1L;

        for (var attempts = 0; attempts < 10; attempts++)
        {
            token.ThrowIfCancellationRequested();

            if (!File.Exists(path))
            {
                await Task.Delay(500, token);
                continue;
            }

            var info = new FileInfo(path);
            var size = info.Length;

            if (size > 0 && size == lastSize)
            {
                return null;
            }

            lastSize = size;
            await Task.Delay(700, token);
        }

        return "Datei ist noch in Bearbeitung.";
    }

    private static string ResolveContentType(string path)
    {
        return Path.GetExtension(path)?.ToLowerInvariant() switch
        {
            ".png" => "image/png",
            ".webp" => "image/webp",
            _ => "image/jpeg",
        };
    }

    private static bool IsRetryableStatus(System.Net.HttpStatusCode statusCode)
    {
        var numeric = (int)statusCode;
        return numeric >= 500 || statusCode is System.Net.HttpStatusCode.RequestTimeout or System.Net.HttpStatusCode.TooManyRequests;
    }

    private static TimeSpan GetRetryDelay(int attempt)
    {
        var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(100, 350));
        return TimeSpan.FromMilliseconds(RetryBaseDelay.TotalMilliseconds * Math.Pow(2, attempt)) + jitter;
    }

    private static async Task<string?> ReadResponseBodyAsync(HttpResponseMessage response, CancellationToken token)
    {
        if (response.Content is null)
        {
            return null;
        }

        var body = await response.Content.ReadAsStringAsync(token);
        if (string.IsNullOrWhiteSpace(body))
        {
            return null;
        }

        body = body.Trim();
        return body.Length > 200 ? body[..200] + "…" : body;
    }

    private static int GetWorkerCount(PhotoboothSettings settings)
    {
        var count = settings.MaxConcurrentUploads;
        if (count < 1)
        {
            return 1;
        }

        return count > 5 ? 5 : count;
    }

    private readonly record struct UploadAttempt(bool Success, bool Retryable, string? Error)
    {
        public static UploadAttempt Ok() => new(true, false, null);

        public static UploadAttempt Fail(string error, bool retryable) => new(false, retryable, error);
    }
}
