using Noctalk.Shared.Chat.Configuration;
using Noctalk_client.Admin;
using Noctalk_client.Client.Conversations;
using Noctalk_client.Client.Networking.Chat;
using Noctalk_client.Client.Server_status;
using System.Net.Http;
using System.Text;
using System.Text.Json;

namespace Noctalk_client.Client.Admin;

public sealed class Admin_discovery : IDisposable
{
    private static readonly TimeSpan Validation_timeout = TimeSpan.FromSeconds(10);
    private static readonly TimeSpan Retry_unreachable_server_after = TimeSpan.FromMinutes(2);
    private static readonly JsonSerializerOptions Json_options = new() { WriteIndented = false };
    private readonly object discoveryLock = new();
    private readonly Conversation_repository conversationRepository;
    private readonly Func<CancellationToken, Task<bool>> ensureTorReady;
    private readonly Func<int> getClientLocalTorPort;
    private readonly Action<string> log;
    private readonly Action<Admin_discovery_update> update;
    private CancellationTokenSource? discoveryCancellationTokenSource;

    public Admin_discovery(Conversation_repository The_conversation_repository, Func<CancellationToken, Task<bool>> The_ensure_tor_ready, Func<int> The_get_client_local_tor_port, Action<string> The_log, Action<Admin_discovery_update> The_update)
    {
        conversationRepository = The_conversation_repository;
        ensureTorReady = The_ensure_tor_ready;
        getClientLocalTorPort = The_get_client_local_tor_port;
        log = The_log;
        update = The_update;
    }

    public void Discover__admin_rights()
    {
        CancellationTokenSource cancellationTokenSource;
        lock (discoveryLock)
        {
            discoveryCancellationTokenSource?.Cancel();
            discoveryCancellationTokenSource?.Dispose();
            cancellationTokenSource = new CancellationTokenSource();
            discoveryCancellationTokenSource = cancellationTokenSource;
            _ = Task.Run(() => Discover__admin_rights__run_async(cancellationTokenSource.Token), CancellationToken.None);
        }
    }

    public void Dispose()
    {
        lock (discoveryLock)
        {
            discoveryCancellationTokenSource?.Cancel();
            discoveryCancellationTokenSource?.Dispose();
            discoveryCancellationTokenSource = null;
        }
    }

    private async Task Discover__admin_rights__run_async(CancellationToken The_cancellation_token)
    {
        try
        {
            List<Conversation_list_item> loadedConversations = conversationRepository.Conversations__load_all();
            Dictionary<string, List<Conversation_config>> candidates = loadedConversations
                .Where(The_conversation => The_conversation.Conversation_config.Admin__guid.HasValue && !string.IsNullOrWhiteSpace(The_conversation.Conversation_config.Server__onion_address))
                .GroupBy(The_conversation => The_conversation.Conversation_config.Server__onion_address.Trim(), StringComparer.OrdinalIgnoreCase)
                .ToDictionary(The_group => The_group.Key, The_group => The_group.Select(The_conversation => The_conversation.Conversation_config).ToList(), StringComparer.OrdinalIgnoreCase);

            if (candidates.Count == 0)
            {
                log($"Admin discovery skipped: no Admin__guid found in {loadedConversations.Count} conversation config(s).");
                update(new Admin_discovery_update { Candidate_server_onion_addresses = [], Flag__completed = true });
                return;
            }

            log($"Admin discovery: started with {candidates.Count} candidate server(s).");
            update(new Admin_discovery_update { Candidate_server_onion_addresses = candidates.Keys.ToList() });
            Dictionary<string, List<Conversation_config>> unreachableCandidates = new(StringComparer.OrdinalIgnoreCase);
            await Discover__admin_rights__validate_candidates_async(candidates, unreachableCandidates, false, The_cancellation_token);
            update(new Admin_discovery_update { Flag__completed = true });

            while (unreachableCandidates.Count > 0 && !The_cancellation_token.IsCancellationRequested)
            {
                await Task.Delay(Retry_unreachable_server_after, The_cancellation_token);
                await Discover__admin_rights__validate_candidates_async(new Dictionary<string, List<Conversation_config>>(unreachableCandidates, StringComparer.OrdinalIgnoreCase), unreachableCandidates, true, The_cancellation_token);
            }
        }
        catch (OperationCanceledException)
        {
        }
        catch (Exception exception)
        {
            log($"Admin discovery failed: {exception.Message}");
            update(new Admin_discovery_update { Flag__completed = true });
        }
    }

    private async Task Discover__admin_rights__validate_candidates_async(Dictionary<string, List<Conversation_config>> The_candidates, Dictionary<string, List<Conversation_config>> The_unreachable_candidates, bool Flag__retry, CancellationToken The_cancellation_token)
    {
        foreach (KeyValuePair<string, List<Conversation_config>> candidate in The_candidates)
        {
            The_cancellation_token.ThrowIfCancellationRequested();
            bool serverValidated = false;
            foreach (Conversation_config conversationConfig in candidate.Value)
            {
                if (!conversationConfig.Admin__guid.HasValue)
                {
                    continue;
                }

                if (!await ensureTorReady(The_cancellation_token))
                {
                    The_unreachable_candidates[candidate.Key] = candidate.Value;
                    Discover__admin_rights__log_unreachable(candidate.Key, Flag__retry);
                    break;
                }

                Admin_validation_result validationResult = await Discover__admin_rights__validate_one_async(candidate.Key, conversationConfig.Admin__guid.Value, getClientLocalTorPort(), The_cancellation_token);
                if (!validationResult.Flag__server_reached)
                {
                    The_unreachable_candidates[candidate.Key] = candidate.Value;
                    Discover__admin_rights__log_unreachable(candidate.Key, Flag__retry);
                    break;
                }

                The_unreachable_candidates.Remove(candidate.Key);
                if (Flag__retry)
                {
                    log("Server did finally respond to admin discovery.");
                }

                if (validationResult.Flag__success)
                {
                    update(new Admin_discovery_update
                    {
                        Server__onion_address = candidate.Key,
                        Admin_server_access = new Admin_server_access
                        {
                            Server__name = validationResult.Server__name,
                            Server__onion_address = candidate.Key,
                            Admin__guid = conversationConfig.Admin__guid.Value,
                            Client__local_tor__port = getClientLocalTorPort()
                        }
                    });
                    serverValidated = true;
                    break;
                }

                update(new Admin_discovery_update
                {
                    Server__onion_address = candidate.Key,
                    Flag__remove_server_access = true
                });
            }

            if (serverValidated)
            {
                continue;
            }
        }
    }

    private async Task<Admin_validation_result> Discover__admin_rights__validate_one_async(string The_server_onion_address, Guid The_admin_guid, int The_client_local_tor_port, CancellationToken The_cancellation_token)
    {
        try
        {
            using CancellationTokenSource timeoutCancellation = CancellationTokenSource.CreateLinkedTokenSource(The_cancellation_token);
            timeoutCancellation.CancelAfter(Validation_timeout);
            using HttpClient httpClient = Chat_poll_client.Chat__create_tor_http_client(The_client_local_tor_port);
            object send = new
            {
                Admin__guid = The_admin_guid,
                Flag__get__conversation_list = true
            };
            string json = JsonSerializer.Serialize(send, Json_options);
            using StringContent content = new(json, Encoding.UTF8, "application/json");
            using HttpResponseMessage response = await httpClient.PostAsync($"http://{The_server_onion_address}:65191/admin", content, timeoutCancellation.Token);
            string responseJson = await response.Content.ReadAsStringAsync(timeoutCancellation.Token);
            using JsonDocument document = JsonDocument.Parse(responseJson);

            return new Admin_validation_result
            {
                Flag__server_reached = true,
                Flag__success = document.RootElement.TryGetProperty("Flag__success", out JsonElement successElement) && successElement.GetBoolean(),
                Server__name = document.RootElement.TryGetProperty("Server__name", out JsonElement serverNameElement) ? serverNameElement.GetString() ?? string.Empty : string.Empty
            };
        }
        catch (OperationCanceledException) when (The_cancellation_token.IsCancellationRequested)
        {
            throw;
        }
        catch
        {
            return new Admin_validation_result { Flag__server_reached = false };
        }
    }

    private void Discover__admin_rights__log_unreachable(string The_server_onion_address, bool Flag__retry)
    {
        if (!Flag__retry)
        {
            log("Did not reach server for admin discovery. Will keep trying...");
        }
    }

    private sealed class Admin_validation_result
    {
        public bool Flag__server_reached { get; set; }

        public bool Flag__success { get; set; }

        public string Server__name { get; set; } = string.Empty;
    }
}
